A modest collection of PHP libraries used at SparkFun.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
6.0 KiB

  1. <?php
  2. namespace SparkLib;
  3. use \MarkdownDocument;
  4. /**
  5. * Wrap up all our usage of Markdown libraries.
  6. *
  7. * Basic usage is like so:
  8. *
  9. * echo Sparkdown::create($source)
  10. * ->allowHtmlTags() // optional - probably in-house only - let HTML through
  11. * ->withCallbacks() // optional - in-house only for now - install callbacks for link shorthand
  12. * ->withMacros() // optional - implies allowHtmlTags() - add a filter to do <!-- foo(bar) --> macros
  13. *
  14. * // optional - adds an additional namespace besides SparkLib\SparkdownMacro to search for macro classes:
  15. * ->addMacroNamespace('\\SomeNameSpace\\SparkdownMacro')
  16. *
  17. * ->getHtml()
  18. *
  19. * If you have something like a model object and you're going to want
  20. * to render it with the same options in more than one place, you may
  21. * want to write a getHtml() wrapper method on that object instead of
  22. * repeating the Sparkdown boilerplate.
  23. */
  24. class Sparkdown {
  25. protected $_md;
  26. protected $_allowHTML = false;
  27. protected $_macros = false;
  28. protected $_macroNamespaces = ['\\SparkLib\\SparkdownMacro'];
  29. protected $_macroContext = [];
  30. /**
  31. * Create a new Sparkdown document from a given source string.
  32. */
  33. public static function create ($source)
  34. {
  35. return new static($source);
  36. }
  37. /**
  38. * Transform a fragment (not necessarily a complete document) of Markdown.
  39. *
  40. * Useful for displaying truncated bits of full documents.
  41. */
  42. public static function transformFragment ($source)
  43. {
  44. return MarkdownDocument::transformFragment($source, MarkdownDocument::NOHTML);
  45. }
  46. public function __construct ($source)
  47. {
  48. $this->_md = MarkdownDocument::createFromString($source);
  49. }
  50. /**
  51. * Toggle pass-through on HTML tags. This is turned off by default
  52. * so that no one will use it on user-supplied data and accidentally
  53. * allow arbitrary HTML from the outside world.
  54. *
  55. * @return Sparkdown
  56. */
  57. public function allowHtmlTags ($allow = true)
  58. {
  59. if (! is_bool($allow)) {
  60. throw new Exception('allowHtmlTags() expects a boolean true/false');
  61. }
  62. if ((! $allow) && $this->_macros)
  63. throw new Exception('withMacros() requires that HTML tags are allowed');
  64. $this->_allowHTML = $allow;
  65. return $this;
  66. }
  67. /**
  68. * Install some callbacks to override link stuff.
  69. *
  70. * Right now, these are only for inhouse users. We can expand
  71. * on this for different sets of users etc.
  72. *
  73. * @return Sparkdown
  74. */
  75. public function withCallbacks ()
  76. {
  77. $this->_md->setUrlCallback(function ($path) {
  78. return $this->urlCallback($path);
  79. });
  80. return $this;
  81. }
  82. /**
  83. * Set a callback for nofollow attributes on links
  84. *
  85. * @return Sparkdown
  86. */
  87. public function setNoFollow ()
  88. {
  89. $this->_md->setAttributesCallback(function () {
  90. return $this->attributesCallback(['rel' => 'nofollow']);
  91. });
  92. return $this;
  93. }
  94. /**
  95. * Return a path for special shorthand in links.
  96. *
  97. * New link shorthand should be defined here, following the pattern
  98. * currently used for tutorials.
  99. *
  100. * A hook for this can be installed by withCallbacks().
  101. */
  102. protected function urlCallback ($path)
  103. {
  104. $m = [];
  105. if (preg_match('/^tutorials?\/([0-9]+)$/', $path, $m)) {
  106. if ($t = \LearnTutorialSaurus::getById($m[1])) {
  107. return \Learn::externalLink('tutorials')->id($t->url_path);
  108. }
  109. }
  110. // we didn't find anything
  111. return null;
  112. }
  113. /**
  114. * Return a rel=nofollow for links
  115. *
  116. * An example hook for this can be seen at setNoFollow().
  117. */
  118. protected function attributesCallback ($attributes)
  119. {
  120. if (is_array($attributes)) {
  121. $attr_str = '';
  122. foreach($attributes as $k => $v) {
  123. $attr_str .= $k . '="' . $v . '" ';
  124. }
  125. return $attr_str;
  126. } else if (is_string($attributes)) {
  127. return $attributes;
  128. }
  129. // unknown attribute type
  130. return null;
  131. }
  132. /**
  133. * Toggle processing macros of the form <!-- macro(param string) -->
  134. *
  135. * Implies allowHtmlTags().
  136. *
  137. * @return Sparkdown
  138. */
  139. public function withMacros ()
  140. {
  141. // necessary for comments to pass through:
  142. $this->allowHtmlTags();
  143. $this->_macros = true;
  144. return $this;
  145. }
  146. /**
  147. * Add a namespace to search for macro classes.
  148. *
  149. * @return Sparkdown
  150. */
  151. public function addMacroNamespace ($namespace)
  152. {
  153. array_unshift($this->_macroNamespaces, $namespace);
  154. return $this;
  155. }
  156. /**
  157. * Set a context array to be passed to macros.
  158. */
  159. public function setMacroContext (array $context)
  160. {
  161. $this->_macroContext = $context;
  162. return $this;
  163. }
  164. /**
  165. * Get the current macro context array.
  166. */
  167. public function getMacroContext ()
  168. {
  169. return $this->_macroContext;
  170. }
  171. /**
  172. * Get the HTML version of the current document.
  173. *
  174. * @return string
  175. */
  176. public function getHtml ()
  177. {
  178. if ($this->_allowHTML) {
  179. $this->_md->compile();
  180. } else {
  181. $this->_md->compile(MarkdownDocument::NOHTML);
  182. }
  183. if ($this->_macros) {
  184. return $this->getHtmlWithMacros();
  185. } else {
  186. return $this->_md->getHtml();
  187. }
  188. }
  189. /**
  190. * Get the HTML version of the current document, with
  191. * <!-- module(param string) --> macros processed.
  192. * Should only be invoked by getHtml() after the document
  193. * has been compiled.
  194. *
  195. * @return string
  196. */
  197. protected function getHtmlWithMacros ()
  198. {
  199. $pat = '/
  200. <!-- [ ] # open comment
  201. (?<name> [_a-z]+) # macro name
  202. \(
  203. (?<input> .*?) # params
  204. \)
  205. [ ] --> # close comment
  206. /x';
  207. $transform = function ($matches) {
  208. foreach ($this->_macroNamespaces as $ns) {
  209. $class = $ns . '\\' . ucfirst($matches['name']);
  210. if ($class_exists = class_exists($class))
  211. break;
  212. }
  213. if (! $class_exists)
  214. return '';
  215. $macro = new $class($matches['input']);
  216. $macro->setContext($this->_macroContext);
  217. return $macro->render();
  218. };
  219. // call $transform() on everything matching $pat in the rendered HTML:
  220. return preg_replace_callback($pat, $transform, $this->_md->getHtml());
  221. }
  222. }