MarkdownPlugin.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <?php
  2. if (!defined('GNUSOCIAL')) {
  3. exit(1);
  4. }
  5. require __DIR__ . '/lib/MarkdownProfileBlock.php';
  6. class MarkdownPlugin extends Plugin
  7. {
  8. const VERSION = '0.1.1';
  9. const NAME_SPACE = 'markdown'; // 'namespace' is a reserved keyword
  10. function initialize()
  11. {
  12. if (!isset($this->parser)) {
  13. $this->parser = 'default';
  14. }
  15. }
  16. // From /lib/util.php::common_render_text
  17. // We don't want to call it directly since we don't want to
  18. // run common_linkify() on the text
  19. function render_text($text)
  20. {
  21. $text = common_remove_unicode_formatting($text);
  22. $text = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $text);
  23. // Link #hashtags
  24. $rendered = preg_replace_callback('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/u',
  25. function ($m) { return "{$m[1]}#".common_tag_link($m[2]); }, $text);
  26. return $rendered;
  27. }
  28. /**
  29. * Replace paragraph tags with double <br>s
  30. *
  31. * Some clients (ex: AndStatus) display extra whitespace when the
  32. * notice is wrapped or ends with a <p> tag (GS doesn't wrap notices in <p> tags)
  33. */
  34. function fix_whitespace($rendered)
  35. {
  36. // Remove <p>s
  37. $rendered = str_replace('<p>', '', $rendered);
  38. // Replace </p>s with <br><br>
  39. $rendered = str_replace('</p>', '<br><br>', $rendered);
  40. // Remove trailing <br><br>
  41. return preg_replace('/<br><br>$/', '', $rendered);
  42. }
  43. /**
  44. * Replace double <br>s with a line-break
  45. *
  46. * The following input:
  47. * Foo
  48. *
  49. * * list
  50. * * item
  51. *
  52. * Has `$notice->rendered` like this:
  53. * Foo<br>
  54. * <br>
  55. * * list<br>
  56. * * item
  57. *
  58. * Turns into this after `common_strip_html()`:
  59. * Foo
  60. *
  61. *
  62. * * list
  63. *
  64. * * item
  65. *
  66. * This method takes the original `$notice->rendered` and makes it like this:
  67. * Foo
  68. *
  69. * * list
  70. * * item
  71. */
  72. function br2nl ($string)
  73. {
  74. // Replace double <br>s with just one EOL
  75. $string = preg_replace('/(\<br(\s*)?\/?\>\n<br(\s*)?\/?\>)/i', PHP_EOL, $string);
  76. // Remove <br>s
  77. return preg_replace('/(\<br(\s*)?\/?\>)/i', '', $string);
  78. }
  79. function onChrStartRenderNotice(&$raw_content, $profile, &$render)
  80. {
  81. $isEnabled = Profile_prefs::getData($profile, MarkdownPlugin::NAME_SPACE, 'enabled', false);
  82. if (!$isEnabled) {
  83. return true;
  84. }
  85. $rendered = common_render_content($raw_content, $profile);
  86. $raw_content = $this->markdownify($rendered);
  87. $render = false;
  88. return true;
  89. }
  90. function markdownify($rendered_str, $notice=null)
  91. {
  92. $text = common_strip_html($this->br2nl($rendered_str), true, true);
  93. if ($this->parser === 'default') {
  94. $rendered = $this->render_text($text);
  95. $rendered = common_replace_urls_callback($text, 'common_linkify');
  96. // handle Markdown links in order not to convert doubly.
  97. $rendered = preg_replace('/\[([^]]+)\]\((<a [^>]+>)([^<]+)<\/a>\)/u', '\2\1</a>', $rendered);
  98. } else {
  99. $rendered = $this->render_text($text);
  100. }
  101. // Some types of notices do not have the hasParent() method,
  102. // but they're not notices we are interested in
  103. if (method_exists($notice, 'hasParent')) {
  104. // Link @mentions, !mentions, @#mentions
  105. $rendered = common_linkify_mentions($rendered, $notice->getProfile(),
  106. $notice->hasParent() ? $notice->getParent() : null);
  107. }
  108. // Prevent leading #hashtags from becoming headers by adding a backslash
  109. // before the "#", telling markdown to leave it alone
  110. $repl_rendered = preg_replace('/^#<span class="tag">/u', '\\\\\\0', $rendered);
  111. // Only use the replaced value from above if it returned a success
  112. if ($rendered !== null) {
  113. $rendered = $repl_rendered;
  114. }
  115. // Convert Markdown to HTML
  116. // TODO: Abstract the parser so we can call the same method regardless of lib
  117. switch($this->parser) {
  118. case 'gfm':
  119. // Composer
  120. require __DIR__ . '/vendor/autoload.php';
  121. $this->parser = new \cebe\markdown\GithubMarkdown();
  122. $rendered = $this->parser->parse($rendered);
  123. break;
  124. default:
  125. $this->parser = new \Michelf\Markdown();
  126. $rendered = $this->parser->defaultTransform($rendered);
  127. }
  128. return common_purify($this->fix_whitespace($rendered));
  129. }
  130. function onStartShowAccountProfileBlock($action, $profile)
  131. {
  132. $markdownProfile = new MarkdownProfileBlock($action, $profile);
  133. $markdownProfile->show();
  134. return false;
  135. }
  136. function onStartNoticeSave($notice)
  137. {
  138. // Only run this on local notices
  139. if ($notice->isLocal()) {
  140. // Get the profile of the user who posted this notice
  141. $profile = Profile::getKV('id', $notice->profile_id);
  142. // Check if they have 'Markdown' enabled in their settings
  143. if ($profile instanceof Profile) {
  144. $isEnabled = Profile_prefs::getData($profile, MarkdownPlugin::NAME_SPACE, 'enabled', false);
  145. } else {
  146. $isEnabled = false;
  147. }
  148. if (!$isEnabled) {
  149. return true;
  150. }
  151. $notice->rendered = $this->markdownify($notice->rendered, $notice);
  152. }
  153. return true;
  154. }
  155. function onEndShowStyles($action)
  156. {
  157. $action->cssLink($this->path('css/markdown.css'));
  158. }
  159. function onEndAccountSettingsNav($action) {
  160. $action->elementStart('li');
  161. $action->element('a', array('href' => common_local_url('markdownsettings')), 'Markdown');
  162. $action->elementEnd('li');
  163. return true;
  164. }
  165. function onRouterInitialized($m)
  166. {
  167. $m->connect(
  168. 'settings/markdownsettings', array(
  169. 'action' => 'markdownsettings'
  170. )
  171. );
  172. return true;
  173. }
  174. function onPluginVersion(array &$versions): bool
  175. {
  176. $versions[] = array('name' => 'Markdown',
  177. 'version' => self::VERSION,
  178. 'author' => 'chimo',
  179. 'homepage' => 'https://github.com/chimo/gs-markdown',
  180. 'description' =>
  181. // TRANS: Plugin description.
  182. _m('Use markdown syntax'));
  183. return true;
  184. }
  185. }