diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ce6751f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,js,json,css,scss,eslintrc}] +indent_size = 2 diff --git a/README.md b/README.md index a50945f..f4246a9 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,101 @@ -Markdown Editor and DBField -================= +# Markdown Editor and DBField -Adds a field and a data type that allows for Markdown editing, uses a supported renderer (default is the github api) to render the html +Adds a field and a data type that allows for Markdown editing, uses a supported renderer (default is the GitHub API) +to render the HTML. ## Requirements -* SilverStripe 3.x -* PHP Curl Support + +* SilverStripe 4.x +* PHP cURL Support ## Installation -* Download the module from here https://github.com/UndefinedOffset/silverstripe-markdown/downloads -* Extract the downloaded archive into your site root so that the destination folder is called markdown, opening the extracted folder should contain _config.php in the root along with other files/folders -* Run dev/build?flush=all to regenerate the manifest -* Upon entering the cms and using MarkdownEditor for the first time you make need to add ?flush=all to the end of the address to force the templates to regenerate + +* Install with [composer](https://getcomposer.org): `composer require undefinedoffset/silverstripe-markdown ^2.0` +* Run `dev/build?flush=all` to regenerate the manifest ## Usage + Use the Markdown data type as your fields data type, then use the MarkdownEditor field in the cms for editing. -### Page class: +### Page class + ```php -class MyPage extends Page { - public static $db=array( - 'MarkdownContent'=>'Markdown' - ); +use UndefinedOffset\Markdown\Forms\MarkdownEditor; + +class MyPage extends Page +{ + public static $db = array( + 'MarkdownContent' => 'Markdown' + ); - public function getCMSFields() { - $fields=parent::getCMSFields(); + public function getCMSFields() + { + $fields = parent::getCMSFields(); $editor = new MarkdownEditor('MarkdownContent', 'Page Content (Markdown)'); $editor->setRows(15); //optional, set number of rows in CMS $editor->setWrapMode(true); //optional, turn on word wrapping - $fields->addFieldToTab("Root.Main", $editor); + $fields->addFieldToTab('Root.Main', $editor); return $fields; } } ``` +### Template -### Template: ```html
- $MarkdownContent + $MarkdownContent
``` -You may also request the markdown using Github Flavored Markdown by calling $YourField.AsHTML(true) in your template by default Github Flavored Markdown is not used just regular Markdown is used. +You may also request the markdown using Github Flavored Markdown by calling $YourField.AsHTML(true) in your template +by default Github Flavored Markdown is not used just regular Markdown is used. + ```html
$MarkdownContent.AsHTML(true)
``` -### Configuration: -The default renderer is the Github renderer. However, other renderers are supported. +### Configuration + +The default renderer is the GitHub renderer. However, other renderers are supported. -To set what renderer to use, in **_config.php** do the following: +To set what renderer to use, in `_config.php` do the following: ```php -Markdown::setRenderer('GithubMarkdownRenderer'); //Class name of any implementation of IMarkdownRenderer will work +use UndefinedOffset\Markdown\Model\FieldTypes\Markdown; + +// Fully qualified (namespaced) class name of any implementation of IMarkdownRenderer will work: +Markdown::setRenderer('UndefinedOffset\\Markdown\\Renderer\\GithubMarkdownRenderer'); ``` #### GithubMarkdownRenderer + The following options are available on the default GithubMarkdownRenderer: + ```php -GithubMarkdownRenderer::useBasicAuth('github username', 'github password'); //authenticate to the Github API to get 5,000 requests per hour instead of 60 -GithubMarkdownRenderer::setUseGFM(true); //whether or not to use Github Flavoured Markdown +use UndefinedOffset\Markdown\Renderer\GitHubMarkdownRenderer; + +// authenticate to the Github API to get 5,000 requests per hour instead of 60 +GithubMarkdownRenderer::useBasicAuth('github username', 'github password'); +// whether or not to use Github Flavoured Markdown +GithubMarkdownRenderer::setUseGFM(true); ``` #### PHPMarkdownMarkdownRenderer -PHPMarkdownMarkdownRenderer is simple and has no options. Use this to avoid the delay on page load the first time after editing that comes from using the Github renderer (especially if the page has many sections of markdown). You will need to install [PHP Markdown](https://github.com/michelf/php-markdown) for this to work - it can be installed with composer. + +PHPMarkdownMarkdownRenderer is simple and has no options. Use this to avoid the delay on page load the first time +after editing that comes from using the Github renderer (especially if the page has many sections of markdown). You +will need to install [PHP Markdown](https://github.com/michelf/php-markdown) for this to work - it can be installed +with composer. **Note:** This renderer does not support Github Flavoured Markdown. + ```php -Markdown::setRenderer('PHPMarkdownMarkdownRenderer'); +use UndefinedOffset\Markdown\Model\FieldTypes\Markdown; + +Markdown::setRenderer('UndefinedOffset\\Markdown\\Renderer\\PHPMarkdownMarkdownRenderer'); ``` diff --git a/_config.php b/_config.php deleted file mode 100644 index b6c5d93..0000000 --- a/_config.php +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/_config/injector.yml b/_config/injector.yml new file mode 100644 index 0000000..9afa918 --- /dev/null +++ b/_config/injector.yml @@ -0,0 +1,6 @@ +--- +Name: markdowninjector +--- +SilverStripe\Core\Injector\Injector: + Markdown: + class: UndefinedOffset\Markdown\Model\FieldTypes\DBMarkdown diff --git a/code/forms/MarkdownEditor.php b/code/forms/MarkdownEditor.php deleted file mode 100644 index 4891144..0000000 --- a/code/forms/MarkdownEditor.php +++ /dev/null @@ -1,48 +0,0 @@ -wrap_mode=$mode; - return $this; - } - - /** - * Returns the field holder used by templates - * @return {string} HTML to be used - */ - public function FieldHolder($properties=array()) { - $this->extraClasses['stacked']='stacked'; - - - Requirements::css(MARKDOWN_MODULE_BASE.'/css/MarkdownEditor.css'); - - Requirements::javascript(MARKDOWN_MODULE_BASE.'/javascript/external/ace/ace.js'); - Requirements::javascript(MARKDOWN_MODULE_BASE.'/javascript/external/ace/mode-markdown.js'); - Requirements::javascript(MARKDOWN_MODULE_BASE.'/javascript/external/ace/theme-textmate.js'); - Requirements::javascript(MARKDOWN_MODULE_BASE.'/javascript/external/ace/theme-twilight.js'); - Requirements::javascript(MARKDOWN_MODULE_BASE.'/javascript/MarkdownEditor.js'); - return parent::FieldHolder($properties); - } - - /** - * Generates the attributes to be used on the field - * @return {array} Array of attributes to be used on the form field - */ - public function getAttributes() { - return array_merge( - parent::getAttributes(), - array( - 'style'=>'width: 97%; max-width: 100%; height: '.($this->rows * 16).'px; resize: none;', // prevents horizontal scrollbars - 'wrap-mode'=>($this->wrap_mode) ? "true" : "false" - ) - ); - } -} -?> \ No newline at end of file diff --git a/code/model/fieldtypes/Markdown.php b/code/model/fieldtypes/Markdown.php deleted file mode 100644 index 6dbfadd..0000000 --- a/code/model/fieldtypes/Markdown.php +++ /dev/null @@ -1,111 +0,0 @@ -'HTMLText', - 'Markdown'=>'Text' - ); - - - public static $escape_type='xml'; - - private static $renderer='GithubMarkdownRenderer'; - - private $renderInst; - - protected $parsedHTML=false; - - - /** - * Checks cache to see if the contents of this field have already been loaded from github, if they haven't then a request is made to the github api to render the markdown - * @param {bool} $useGFM Use Github Flavored Markdown or render using plain markdown defaults to false just like how readme files are rendered on github - * @return {string} Markdown rendered as HTML - */ - public function AsHTML($useGFM=false) { - if($this->parsedHTML!==false) { - return $this->parsedHTML; - } - - //Setup renderer - $renderer=$this->getRenderer(); - $supported=$renderer->isSupported(); - if($supported!==true) { - $class_name=get_class($renderer); - user_error("Renderer $class_name is not supported on this system: $supported"); - } - - if($renderer instanceof GithubMarkdownRenderer) { - $beforeUseGFM=GithubMarkdownRenderer::getUseGFM(); - - GithubMarkdownRenderer::setUseGFM($useGFM); - } - - //Init cache stuff - $cacheKey=md5('Markdown_'.$this->tableName.'_'.$this->name.':'.$this->value); - $cache=SS_Cache::factory('Markdown'); - $cachedHTML=$cache->load($cacheKey); - - //Check cache, if it's good use it instead - if($cachedHTML!==false) { - $this->parsedHTML=$cachedHTML; - return $this->parsedHTML; - } - - //If empty save time by not attempting to render - if(empty($this->value)) { - return $this->value; - } - - //Get rendered HTML - $response=$renderer->getRenderedHTML($this->value); - - //Store response in memory - $this->parsedHTML=$response; - - //Cache response to file system - $cache->save($this->parsedHTML, $cacheKey); - - //Reset GFM - if($renderer instanceof GithubMarkdownRenderer) { - GithubMarkdownRenderer::setUseGFM($beforeUseGFM); - } - - //Return response - return $this->parsedHTML; - } - - /** - * Renders the field used in the template - * @return {string} HTML to be used in the template - * - * @see GISMarkdown::AsHTML() - */ - public function forTemplate() { - return $this->AsHTML(); - } - - /** - * Sets the renderer for markdown fields to use - * @param {string} $renderer Class Name of an implementation of IMarkdownRenderer - */ - public static function setRenderer($renderer) { - if(ClassInfo::classImplements($renderer, 'IMarkdownRenderer')) { - self::$renderer=$renderer; - }else { - user_error('The renderer '.$renderer.' does not implement IMarkdownRenderer', E_USER_ERROR); - } - } - - /** - * Gets the active mardown renderer - * @return {IMarkdownRenderer} An implementation of IMarkdownRenderer - */ - private function getRenderer() { - if(!is_object($this->renderInst)) { - $class=self::$renderer; - $this->renderInst=new $class(); - } - - return $this->renderInst; - } -} -?> \ No newline at end of file diff --git a/code/renderer/GithubMarkdownRenderer.php b/code/renderer/GithubMarkdownRenderer.php deleted file mode 100644 index 69ce0a0..0000000 --- a/code/renderer/GithubMarkdownRenderer.php +++ /dev/null @@ -1,91 +0,0 @@ -text=$value; - $sendObj->mode=(self::$useGFM ? 'gmf':'markdown'); - $content=json_encode($sendObj); - - //Build headers - $headers=array("Content-type: application/json", "User-Agent: curl"); - if(self::$useBasicAuth) { - $encoded=base64_encode(self::$username.':'.self::$password); - $headers[]="Authorization: Basic $encoded"; - } - - //Build curl request to github's api - $curl=curl_init('https://api.github.com/markdown'); - curl_setopt($curl, CURLOPT_HEADER, false); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_POST, true); - curl_setopt($curl, CURLOPT_POSTFIELDS, $content); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - - - //Send request and verify response - $response=curl_exec($curl); - $status=curl_getinfo($curl, CURLINFO_HTTP_CODE); - if($status!=200) { - user_error("Error: Call to api.github.com failed with status $status, response $response, curl_error ".curl_error($curl).", curl_errno ".curl_errno($curl), E_USER_WARNING); - } - - //Close curl connection - curl_close($curl); - - return $response; - } - - /** - * Globally enable or disable github flavored markdown - * @param {bool} $val Boolean true to enable false otherwise - */ - public static function setUseGFM($value=true) { - self::$useGFM=$value; - } - - /** - * Gets if github flavored markdown is enabled or not globally - * @return {bool} Returns boolean true if github flavored markdown is enabled false otherwise - */ - public static function getUseGFM() { - return self::$useGFM; - } - - /** - * Sets whether or not to include the Authorization header in GitHub API requests, both parameters are required to enable basic auth - * @param {string} $username Github Username - * @param {string} $password Github Password - */ - public function useBasicAuth($username=false, $password=false) { - self::$useBasicAuth=($username!==false && $password!==false); - self::$username=$username; - self::$password=$password; - } -} -?> \ No newline at end of file diff --git a/code/renderer/IMarkdownRenderer.php b/code/renderer/IMarkdownRenderer.php deleted file mode 100644 index 4b42297..0000000 --- a/code/renderer/IMarkdownRenderer.php +++ /dev/null @@ -1,16 +0,0 @@ - \ No newline at end of file diff --git a/code/renderer/PHPMarkdownMarkdownRenderer.php b/code/renderer/PHPMarkdownMarkdownRenderer.php deleted file mode 100644 index f57d370..0000000 --- a/code/renderer/PHPMarkdownMarkdownRenderer.php +++ /dev/null @@ -1,25 +0,0 @@ - \ No newline at end of file diff --git a/composer.json b/composer.json index cc7a66b..9e693ba 100644 --- a/composer.json +++ b/composer.json @@ -1,26 +1,38 @@ { - "name": "undefinedoffset/silverstripe-markdown", - "description": "Adds a field and a data type that allows for Markdown editing, uses the github api to render the html", - "type": "silverstripe-module", - "keywords": ["silverstripe", "markdown"], - "license": "BSD-3-Clause", - "authors": [ - { - "name": "Ed Chipman", - "homepage": "http://www.edchipman.ca", - "role": "Developer" - } - ], + "name": "undefinedoffset/silverstripe-markdown", + "description": "Adds a field and a data type that allows for Markdown editing, uses the github api to render the html", + "type": "silverstripe-vendormodule", + "keywords": ["silverstripe", "markdown"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Ed Chipman", + "homepage": "http://www.edchipman.ca", + "role": "Developer" + } + ], - "require": - { - "silverstripe/framework": "3.*", - "composer/installers": "*" - }, - "support": { - "issues": "https://github.com/undefinedoffset/silverstripe-markdown/issues" - }, - "extra": { - "installer-name": "markdown" - } + "require": { + "silverstripe/framework": "^4.0", + "michelf/php-markdown": "^1.8", + "ext-json": "*", + "ext-curl": "*" + }, + "support": { + "issues": "https://github.com/undefinedoffset/silverstripe-markdown/issues" + }, + "extra": { + "branch-alias": { + "dev-ss4-compat": "2.x-dev" + }, + "expose": [ + "css", + "javascript" + ] + }, + "autoload": { + "psr-4": { + "UndefinedOffset\\Markdown\\": "src/" + } + } } diff --git a/src/Forms/MarkdownEditor.php b/src/Forms/MarkdownEditor.php new file mode 100644 index 0000000..19d5aed --- /dev/null +++ b/src/Forms/MarkdownEditor.php @@ -0,0 +1,70 @@ +wrap_mode = $mode; + + return $this; + } + + /** + * Returns the field holder used by templates + * @return string HTML to be used + */ + public function FieldHolder($properties = []) + { + $this->extraClasses['stacked'] = 'stacked'; + + Requirements::css('undefinedoffset/silverstripe-markdown: css/MarkdownEditor.css'); + + Requirements::javascript('undefinedoffset/silverstripe-markdown: javascript/external/ace/ace.js'); + Requirements::javascript('undefinedoffset/silverstripe-markdown: javascript/external/ace/mode-markdown.js'); + Requirements::javascript('undefinedoffset/silverstripe-markdown: javascript/external/ace/theme-textmate.js'); + Requirements::javascript('undefinedoffset/silverstripe-markdown: javascript/external/ace/theme-twilight.js'); + Requirements::javascript('undefinedoffset/silverstripe-markdown: javascript/MarkdownEditor.js'); + + return parent::FieldHolder($properties); + } + + /** + * Generates the attributes to be used on the field + * @return array Array of attributes to be used on the form field + */ + public function getAttributes() + { + return array_merge( + parent::getAttributes(), + [ + // prevents horizontal scrollbars + 'style' => 'width: 97%; max-width: 100%; height: ' . ($this->rows * 16) . 'px; resize: none;', + 'wrap-mode' => ($this->wrap_mode) ? 'true' : 'false', + ] + ); + } +} diff --git a/src/Model/FieldTypes/DBMarkdown.php b/src/Model/FieldTypes/DBMarkdown.php new file mode 100644 index 0000000..aa1f11a --- /dev/null +++ b/src/Model/FieldTypes/DBMarkdown.php @@ -0,0 +1,156 @@ + 'HTMLText', + 'Markdown' => 'DBText', + ]; + + /** + * @var string + */ + private static $escape_type = 'xml'; + + /** + * @var string + */ + private static $renderer = GithubMarkdownRenderer::class; + + /** + * @var IMarkdownRenderer + */ + private $renderInst; + + /** + * @var string + */ + protected $parsedHTML = false; + + + /** + * Checks cache to see if the contents of this field have already been loaded from github, if they haven't + * then a request is made to the github api to render the markdown + * @param bool $useGFM Use Github Flavored Markdown or render using plain markdown defaults to false just like + * how readme files are rendered on github + * @return string Markdown rendered as HTML + */ + public function AsHTML($useGFM = false) + { + if ($this->parsedHTML !== false) { + return $this->parsedHTML; + } + + //Setup renderer + $renderer = $this->getRenderer(); + $supported = $renderer->isSupported(); + if ($supported !== true) { + $class_name = get_class($renderer); + user_error("Renderer $class_name is not supported on this system: $supported"); + } + + if ($renderer instanceof GithubMarkdownRenderer) { + $beforeUseGFM = GithubMarkdownRenderer::getUseGFM(); + + GithubMarkdownRenderer::setUseGFM($useGFM); + } + + //Init cache stuff + /*$cacheKey = $this->getCacheKey(); + $cache = Injector::inst()->get(CacheInterface::class . '.markdown'); + $cachedHTML = $cache->load($cacheKey);//*/ + + //Check cache, if it's good use it instead + if (isset($cachedHTML) && $cachedHTML !== false) { + $this->parsedHTML = $cachedHTML; + + return $this->parsedHTML; + } + + //If empty save time by not attempting to render + if (empty($this->value)) { + return $this->value; + } + + //Get rendered HTML + $response = $renderer->getRenderedHTML($this->value); + + //Store response in memory + $this->parsedHTML = $response; + + //Cache response to file system + if (isset($cache) && isset($cacheKey)) { + $cache->save($this->parsedHTML, $cacheKey); + } + + //Reset GFM + if ($renderer instanceof GithubMarkdownRenderer && isset($beforeUseGFM)) { + GithubMarkdownRenderer::setUseGFM($beforeUseGFM); + } + + //Return response + return $this->parsedHTML; + } + + /** + * Renders the field used in the template + * @return string HTML to be used in the template + * + * @see GISMarkdown::AsHTML() + */ + public function forTemplate() + { + return $this->AsHTML(); + } + + /** + * Sets the renderer for markdown fields to use + * @param string $renderer Class Name of an implementation of IMarkdownRenderer + */ + public static function setRenderer($renderer) + { + if (ClassInfo::classImplements($renderer, IMarkdownRenderer::class)) { + self::$renderer = $renderer; + } else { + user_error('The renderer ' . $renderer . ' does not implement IMarkdownRenderer', E_USER_ERROR); + } + } + + /** + * Gets the active markdown renderer + * @return IMarkdownRenderer An implementation of IMarkdownRenderer + */ + private function getRenderer() + { + if (!is_object($this->renderInst)) { + $class = self::$renderer; + $this->renderInst = new $class(); + } + + return $this->renderInst; + } + + /** + * @return string + */ + public function getCacheKey() + { + return md5('Markdown_' . $this->tableName . '_' . $this->name . '_' . $this->value); + } +} diff --git a/src/Renderer/GithubMarkdownRenderer.php b/src/Renderer/GithubMarkdownRenderer.php new file mode 100644 index 0000000..c0d12ac --- /dev/null +++ b/src/Renderer/GithubMarkdownRenderer.php @@ -0,0 +1,126 @@ +text = $value; + $sendObj->mode = (self::$useGFM ? 'gfm' : 'markdown'); + $content = json_encode($sendObj); + + //Build headers + $headers = ['Content-type: application/json', 'User-Agent: curl']; + if (self::$useBasicAuth) { + $encoded = base64_encode(self::$username . ':' . self::$password); + $headers[] = "Authorization: Basic $encoded"; + } + + //Build curl request to github's api + $curl = curl_init('https://api.github.com/markdown'); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $content); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + + //Send request and verify response + $response = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + if ($status != 200) { + user_error( + "Error: Call to api.github.com failed with status $status, response $response, curl_error " + . curl_error($curl) . ", curl_errno " . curl_errno($curl), + E_USER_WARNING + ); + } + + //Close curl connection + curl_close($curl); + + return $response; + } + + /** + * Globally enable or disable github flavored markdown + * @param bool $value Boolean true to enable false otherwise + */ + public static function setUseGFM($value = true) + { + self::$useGFM = $value; + } + + /** + * Gets if github flavored markdown is enabled or not globally + * @return bool Returns boolean true if github flavored markdown is enabled false otherwise + */ + public static function getUseGFM() + { + return self::$useGFM; + } + + /** + * Sets whether or not to include the Authorization header in GitHub API requests, both parameters are + * required to enable basic auth + * @param bool|string $username Github Username + * @param bool|string $password Github Password + */ + public function useBasicAuth($username = false, $password = false) + { + self::$useBasicAuth = ($username !== false && $password !== false); + self::$username = $username; + self::$password = $password; + } +} diff --git a/src/Renderer/IMarkdownRenderer.php b/src/Renderer/IMarkdownRenderer.php new file mode 100644 index 0000000..552b110 --- /dev/null +++ b/src/Renderer/IMarkdownRenderer.php @@ -0,0 +1,25 @@ +