<?php namespace HashOver;

// Copyright (C) 2015-2019 Jacob Barkdull
// This file is part of HashOver.
//
// HashOver is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// HashOver is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with HashOver.  If not, see <http://www.gnu.org/licenses/>.


class Markdown
{
	// Matches a markdown code block
	protected $blockCodeRegex = '/```([\s\S]+?)```/S';

	// Matches a paragraph/double line break
	protected $paragraphRegex = '/(?:\r\n|\r|\n){2}/S';

	// Matches markdown inline code
	protected $inlineCodeRegex = '/(^|[^a-z0-9`])`((?!`)[\s\S]+?)`([^a-z0-9`]|$)/iS';

	// Array for inline code and code block markers
	protected $codeMarkers = array (
		'block' => array ('marks' => array (), 'count' => 0),
		'inline' => array ('marks' => array (), 'count' => 0)
	);

	// Markdown patterns to search for
	protected $search = array (
		// Matches **bold** text
		'/\*\*([^ *])([\s\S]+?)([^ *])\*\*/S',

		// Matches *italic* text
		'/\*([^ *])([\s\S]+?)([^ *])\*/S',

		// Matches _underlined_ text
		'/(^|\W)_((?!_)[\s\S]+?)_(\W|$)/S',

		// Matches forced __underlined__ text
		'/__([^ _])([\s\S]+?)([^ _])__/S',

		// Matches ~~strikethrough~~ text
		'/~~([^ ~])([\s\S]+?)([^ ~])~~/S'
	);

	// HTML replacements for markdown patterns
	protected $replace = array (
		'<strong>\\1\\2\\3</strong>',
		'<em>\\1\\2\\3</em>',
		'\\1<u>\\2</u>\\3',
		'<u>\\1\\2\\3</u>',
		'<s>\\1\\2\\3</s>'
	);

	// Replaces markdown for inline code with a marker
	protected function codeReplace ($grp, $display)
	{
		$markName = 'CODE_' . strtoupper ($display);
		$markCount = $this->codeMarkers[$display]['count']++;

		if ($display !== 'block') {
			$codeMarker = $grp[1] . $markName . '[' . $markCount . ']' . $grp[3];
			$this->codeMarkers[$display]['marks'][$markCount] = trim ($grp[2], "\r\n");
		} else {
			$codeMarker = $markName . '[' . $markCount . ']';
			$this->codeMarkers[$display]['marks'][$markCount] = trim ($grp[1], "\r\n");
		}

		return $codeMarker;
	}

	// Replaces markdown for code block with a marker
	protected function blockCodeReplace ($grp)
	{
		return $this->codeReplace ($grp, 'block');
	}

	// Replaces markdown for inline code with a marker
	protected function inlineCodeReplace ($grp)
	{
		return $this->codeReplace ($grp, 'inline');
	}

	// Returns the original inline markdown code with HTML replacement
	protected function inlineCodeReturn ($grp)
	{
		return '<code class="hashover-inline">' . $this->codeMarkers['inline']['marks'][($grp[1])] . '</code>';
	}

	// Returns the original markdown code block with HTML replacement
	protected function blockCodeReturn ($grp)
	{
		return '<code>' . $this->codeMarkers['block']['marks'][($grp[1])] . '</code>';
	}

	// Parses a string as markdown
	public function parseMarkdown ($string)
	{
		// Reset marker arrays
		$this->codeMarkers = array (
			'block' => array ('marks' => array (), 'count' => 0),
			'inline' => array ('marks' => array (), 'count' => 0)
		);

		// Replace code blocks with markers
		$string = preg_replace_callback ($this->blockCodeRegex, 'self::blockCodeReplace', $string);

		// Break string into paragraphs
		$paragraphs = preg_split ($this->paragraphRegex, $string);

		// Run through each paragraph
		for ($i = 0, $il = count ($paragraphs); $i < $il; $i++) {
			// Replace inline code with markers
			$paragraphs[$i] = preg_replace_callback ($this->inlineCodeRegex, 'self::inlineCodeReplace', $paragraphs[$i]);

			// Replace markdown patterns
			$paragraphs[$i] = preg_replace ($this->search, $this->replace, $paragraphs[$i]);

			// Replace markers with original markdown code
			$paragraphs[$i] = preg_replace_callback ('/CODE_INLINE\[([0-9]+)\]/S', 'self::inlineCodeReturn', $paragraphs[$i]);
		}

		// Join paragraphs
		$string = implode (PHP_EOL . PHP_EOL, $paragraphs);

		// Replace code block markers with original markdown code
		$string = preg_replace_callback ('/CODE_BLOCK\[([0-9]+)\]/S', 'self::blockCodeReturn', $string);

		return $string;
	}
}