markdown - Version 1.1.0

Version Notes

https://github.com/SchumacherFM/Magento-Markdown

Download this release

Release Info

Developer Cyrill Schumacher
Extension markdown
Version 1.1.0
Comparing to
See all releases


Version 1.1.0

app/code/community/SchumacherFM/Markdown/.DS_Store ADDED
Binary file
app/code/community/SchumacherFM/Markdown/Helper/Data.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @category SchumacherFM_Markdown
4
+ * @package Helper
5
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
6
+ * @copyright Copyright (c)
7
+ */
8
+ class SchumacherFM_Markdown_Helper_Data extends Mage_Core_Helper_Abstract
9
+ {
10
+
11
+ /**
12
+ * easy access method for rendering markdown in phtml files
13
+ * usage:
14
+ * echo Mage::helper('markdown')->render($_product->getDescription())
15
+ *
16
+ * @param string $text
17
+ * @param array $options
18
+ *
19
+ * @return string
20
+ */
21
+ public function render($text,array $options = null)
22
+ {
23
+ return Mage::getSingleton('markdown/markdown_render')
24
+ ->setOptions($options)
25
+ ->renderMarkdown($text, TRUE);
26
+ }
27
+
28
+ /**
29
+ * @todo if backend check for current selected store view / website
30
+ *
31
+ * @return bool
32
+ */
33
+ public function getDetectionTag()
34
+ {
35
+ return Mage::getStoreConfig('schumacherfm/markdown/detection_tag');
36
+ }
37
+
38
+ /**
39
+ * @todo if backend check for current selected store view / website
40
+ *
41
+ * check if MD is enabled ... per store view
42
+ *
43
+ * @return bool
44
+ */
45
+ public function isDisabled()
46
+ {
47
+ return !(boolean)Mage::getStoreConfig('schumacherfm/markdown/enable');
48
+ }
49
+
50
+ /**
51
+ * @todo if backend check for current selected store view / website
52
+ * check if md extra is enabled ... per store view
53
+ *
54
+ * @return bool
55
+ */
56
+ public function isMarkdownExtra()
57
+ {
58
+ return (boolean)Mage::getStoreConfig('schumacherfm/markdown/md_extra');
59
+ }
60
+ }
app/code/community/SchumacherFM/Markdown/Model/Editor/Config.php ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @category SchumacherFM_Markdown
4
+ * @package Model
5
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
6
+ * @copyright Copyright (c)
7
+ */
8
+ class SchumacherFM_Markdown_Model_Editor_Config
9
+ {
10
+ /**
11
+ * Prepare variable wysiwyg config
12
+ *
13
+ * @param Varien_Object $config
14
+ *
15
+ * @return array
16
+ */
17
+ public function getWysiwygPluginSettings($config)
18
+ {
19
+ $variableConfig = array();
20
+ $onclickParts = array(
21
+ 'search' => array('html_id'),
22
+ 'subject' => 'renderMarkdown(\'{{html_id}}\');'
23
+ );
24
+ $onclickPartsSyntax = array(
25
+ 'search' => array('html_id'),
26
+ 'subject' => 'markdownSyntax(\'http://daringfireball.net/projects/markdown/syntax\',\'{{html_id}}\');'
27
+ );
28
+ $variableWysiwygPlugin = array(
29
+ array(
30
+ 'name' => 'markdownToggle',
31
+ 'src' => '',
32
+ 'options' => array(
33
+ 'title' => Mage::helper('markdown')->__('MD enable'),
34
+ 'url' => '',
35
+ 'onclick' => array(
36
+ 'search' => array('html_id'),
37
+ 'subject' => 'toggleMarkdown(\''.
38
+ rawurlencode(Mage::helper('markdown')->getDetectionTag())
39
+ .'\',\'{{html_id}}\');'
40
+ ),
41
+ 'class' => 'plugin'
42
+ )
43
+ ),
44
+ array(
45
+ 'name' => 'markdown',
46
+ 'src' => '',
47
+ 'options' => array(
48
+ 'title' => Mage::helper('markdown')->__('MD Preview'),
49
+ 'url' => '',
50
+ 'onclick' => $onclickParts,
51
+ 'class' => 'plugin'
52
+ )
53
+ ),
54
+ array(
55
+ 'name' => 'markdownsyntax',
56
+ 'src' => '',
57
+ 'options' => array(
58
+ 'title' => Mage::helper('markdown')->__('MD Syntax'),
59
+ 'url' => '',
60
+ 'onclick' => $onclickPartsSyntax,
61
+ 'class' => 'plugin'
62
+ )
63
+ ),
64
+ );
65
+
66
+ if (Mage::helper('markdown')->isMarkdownExtra()) {
67
+ $variableWysiwygPlugin[] = array(
68
+ 'name' => 'markdownextrasyntax',
69
+ 'src' => '',
70
+ 'options' => array(
71
+ 'title' => Mage::helper('markdown')->__('MD Extra Syntax'),
72
+ 'url' => '',
73
+ 'onclick' => array(
74
+ 'search' => array('html_id'),
75
+ 'subject' => 'markdownSyntax(\'http://michelf.ca/projects/php-markdown/extra/\',\'{{html_id}}\');'
76
+ ),
77
+ 'class' => 'plugin'
78
+ )
79
+ );
80
+ }
81
+
82
+ $configPlugins = $config->getData('plugins');
83
+ $variableConfig['plugins'] = array_merge($configPlugins, $variableWysiwygPlugin);
84
+ return $variableConfig;
85
+ }
86
+
87
+ }
app/code/community/SchumacherFM/Markdown/Model/Editor/Observer.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @category SchumacherFM_Markdown
4
+ * @package Model
5
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
6
+ * @copyright Copyright (c)
7
+ */
8
+ class SchumacherFM_Markdown_Model_Editor_Observer
9
+ {
10
+ /**
11
+ * Add markdown wysiwyg plugin config
12
+ *
13
+ * @param Varien_Event_Observer $observer
14
+ *
15
+ * @return SchumacherFM_Markdown_Model_Editor_Observer
16
+ */
17
+ public function prepareWysiwygPluginConfig(Varien_Event_Observer $observer)
18
+ {
19
+ if (Mage::helper('markdown')->isDisabled()) {
20
+ return null;
21
+ }
22
+
23
+ $config = $observer->getEvent()->getConfig();
24
+ $settings = Mage::getModel('markdown/editor_config')->getWysiwygPluginSettings($config);
25
+ $config->addData($settings);
26
+ return $this;
27
+ }
28
+
29
+ /**
30
+ * is Markdown is enabled then disable completely the wysiwyg editor
31
+ *
32
+ * @param Varien_Event_Observer $observer
33
+ */
34
+ public function enableDisableWysiwyg(Varien_Event_Observer $observer)
35
+ {
36
+ /** @var Mage_Core_Model_Config_Data $data */
37
+ $data = $observer->getEvent()->getDataObject()->getData();
38
+
39
+ if (isset($data['groups']['markdown']) && isset($data['groups']['markdown']['fields']['enable'])) {
40
+ // @todo watch for store code ...
41
+ $isEnabled = (boolean)$data['groups']['markdown']['fields']['enable']['value'];
42
+ $configurationModel = Mage::getModel('core/config');
43
+ $configurationModel->saveConfig('cms/wysiwyg/enabled', $isEnabled ? 'disabled' : 'enabled');
44
+ }
45
+ }
46
+ }
app/code/community/SchumacherFM/Markdown/Model/Markdown/Render.php ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @category SchumacherFM_Markdown
4
+ * @package Model
5
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
6
+ * @copyright Copyright (c)
7
+ */
8
+ class SchumacherFM_Markdown_Model_Markdown_Render
9
+ {
10
+ private $_tag = '';
11
+ private $_isDisabled = FALSE;
12
+
13
+ /**
14
+ * @var string
15
+ */
16
+ private $_currentRenderedText = '';
17
+
18
+ private $_preserveContainer = array();
19
+
20
+ /**
21
+ * @var SchumacherFM_Markdown_Model_Michelf_Markdown
22
+ */
23
+ private $_renderer = null;
24
+
25
+ protected $_options = array();
26
+
27
+ public function __construct()
28
+ {
29
+ /**
30
+ * due to some weired parsings ... every text field which should contain MD must start with this tag
31
+ */
32
+ $this->_tag = Mage::helper('markdown')->getDetectionTag();
33
+ $this->_isDisabled = Mage::helper('markdown')->isDisabled();
34
+
35
+ $isExtra = Mage::helper('markdown')->isMarkdownExtra() ? '_extra' : '';
36
+ $this->_renderer = Mage::getModel('markdown/michelf_markdown' . $isExtra);
37
+ }
38
+
39
+ /**
40
+ * @return SchumacherFM_Markdown_Model_Michelf_Markdown
41
+ */
42
+ public function getRenderer()
43
+ {
44
+ return $this->_renderer;
45
+ }
46
+
47
+ /**
48
+ * @param array $options
49
+ *
50
+ * @return $this
51
+ */
52
+ public function setOptions(array $options = null)
53
+ {
54
+ $this->_options = $options;
55
+ return $this;
56
+ }
57
+
58
+ /**
59
+ * @param string $text
60
+ *
61
+ * @return string
62
+ */
63
+ public function renderMarkdown($text)
64
+ {
65
+ return $this->_isDisabled
66
+ ? $text
67
+ : $this->_renderMarkdown($text);
68
+ }
69
+
70
+ /**
71
+ * @param Varien_Event_Observer $observer
72
+ *
73
+ * @return $this
74
+ */
75
+ public function renderPageObserver(Varien_Event_Observer $observer)
76
+ {
77
+ if ($this->_isDisabled) {
78
+ return null;
79
+ }
80
+
81
+ /** @var Mage_Cms_Model_Page $page */
82
+ $page = $observer->getEvent()->getPage();
83
+
84
+ if ($page instanceof Mage_Cms_Model_Page) {
85
+ $this->setOptions(array(
86
+ 'force' => FALSE,
87
+ 'protectMagento' => TRUE,
88
+ ));
89
+ $content = $this->_renderMarkdown($page->getContent());
90
+ $page->setContent($content);
91
+ }
92
+
93
+ return $this;
94
+ }
95
+
96
+ /**
97
+ * renders every block as markdown except those having the html tags of method _isMarkdown in it
98
+ *
99
+ * @param Varien_Event_Observer $observer
100
+ *
101
+ * @return $this
102
+ */
103
+ public function renderBlockObserver(Varien_Event_Observer $observer)
104
+ {
105
+ if ($this->_isDisabled) {
106
+ return null;
107
+ }
108
+
109
+ /** @var Mage_Cms_Model_Page $page */
110
+ $block = $observer->getEvent()->getBlock();
111
+
112
+ if ($this->_isAllowedBlock($block)) {
113
+ /** @var Varien_Object $transport */
114
+ $transport = $observer->getEvent()->getTransport();
115
+
116
+ /**
117
+ * you can set on any block the property ->setData('is_markdown',true)
118
+ * then the block will get rendered as markdown even if it contains html
119
+ */
120
+ $isMarkdown = (boolean)$block->getIsMarkdown();
121
+ $this->setOptions(array(
122
+ 'force' => $isMarkdown,
123
+ 'protectMagento' => FALSE,
124
+ ));
125
+ $html = $transport->getHtml();
126
+ $transport->setHtml($this->_renderMarkdown($html));
127
+
128
+ }
129
+ return $this;
130
+ }
131
+
132
+ /**
133
+ * @param string $text
134
+ *
135
+ * @return string
136
+ */
137
+ protected function _renderMarkdown($text)
138
+ {
139
+ $force = isset($this->_options['force']) && $this->_options['force'] === TRUE;
140
+ $protectMagento = isset($this->_options['protectMagento']) && $this->_options['protectMagento'] === TRUE;
141
+ $this->_currentRenderedText = $text;
142
+ if (!$this->_isMarkdown() && $force === FALSE) {
143
+ return $this->_currentRenderedText;
144
+ }
145
+
146
+ $this->_removeMarkdownTag();
147
+ if ($protectMagento) {
148
+ $this->_preserveMagentoVariablesEncode();
149
+ }
150
+ $this->_currentRenderedText = $this->getRenderer()->defaultTransform($this->_currentRenderedText);
151
+ if ($protectMagento) {
152
+ $this->_preserveMagentoVariablesDecode();
153
+ }
154
+ return $this->_currentRenderedText;
155
+ }
156
+
157
+ /**
158
+ * removes the markdown detection tag
159
+ *
160
+ * @return $this
161
+ */
162
+ protected function _removeMarkdownTag()
163
+ {
164
+ $this->_currentRenderedText = str_replace($this->_tag, '', $this->_currentRenderedText);
165
+ return $this;
166
+ }
167
+
168
+ /**
169
+ * @return $this
170
+ */
171
+ protected function _preserveMagentoVariablesEncode()
172
+ {
173
+ $matches = array();
174
+ preg_match_all('~(\{\{[a-z]+.+\}\})~ismU', $this->_currentRenderedText, $matches, PREG_SET_ORDER);
175
+ if (count($matches) > 0) {
176
+ foreach ($matches as $match) {
177
+ $key = md5($match[0]);
178
+ $this->_preserveContainer[$key] = $match[0];
179
+ }
180
+ $this->_currentRenderedText = str_replace(
181
+ $this->_preserveContainer, array_keys($this->_preserveContainer), $this->_currentRenderedText
182
+ );
183
+ }
184
+ return $this;
185
+ }
186
+
187
+ /**
188
+ * @return $this
189
+ */
190
+ protected function _preserveMagentoVariablesDecode()
191
+ {
192
+ if (count($this->_preserveContainer) === 0) {
193
+ return $this;
194
+ }
195
+ $this->_currentRenderedText = str_replace(
196
+ array_keys($this->_preserveContainer), $this->_preserveContainer, $this->_currentRenderedText
197
+ );
198
+ $this->_preserveContainer = array();
199
+ return $this;
200
+ }
201
+
202
+ /**
203
+ * checks if text contains no html ... if so considered as markdown ... not a nice way...
204
+ *
205
+ * @return bool
206
+ */
207
+ protected function _isMarkdown()
208
+ {
209
+ $flag = !empty($this->_currentRenderedText);
210
+ return $flag === TRUE && strpos($this->_currentRenderedText, $this->_tag) !== FALSE;
211
+ }
212
+
213
+ /**
214
+ * @param Mage_Core_Block_Abstract $block
215
+ *
216
+ * @return bool
217
+ */
218
+ protected function _isAllowedBlock($block)
219
+ {
220
+ return $block instanceof Mage_Core_Block_Abstract;
221
+ }
222
+
223
+ }
app/code/community/SchumacherFM/Markdown/Model/Michelf/Markdown.php ADDED
@@ -0,0 +1,1545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ #
3
+ # Markdown - A text-to-HTML conversion tool for web writers
4
+ #
5
+ # PHP Markdown
6
+ # Copyright (c) 2004-2013 Michel Fortin
7
+ # <http://michelf.com/projects/php-markdown/>
8
+ #
9
+ # Original Markdown
10
+ # Copyright (c) 2004-2006 John Gruber
11
+ # <http://daringfireball.net/projects/markdown/>
12
+ #
13
+ # Markdown Parser Class
14
+ #
15
+
16
+ class SchumacherFM_Markdown_Model_Michelf_Markdown
17
+ {
18
+
19
+ ### Version ###
20
+
21
+ const MARKDOWNLIB_VERSION = "1.3";
22
+
23
+ ### Simple Function Interface ###
24
+
25
+ public static function defaultTransform($text)
26
+ {
27
+ #
28
+ # Initialize the parser and return the result of its transform method.
29
+ # This will work fine for derived classes too.
30
+ #
31
+ # Take parser class on which this function was called.
32
+ $parser_class = \get_called_class();
33
+
34
+ # try to take parser from the static parser list
35
+ static $parser_list;
36
+ $parser =& $parser_list[$parser_class];
37
+
38
+ # create the parser it not already set
39
+ if (!$parser)
40
+ $parser = new $parser_class;
41
+
42
+ # Transform text using parser.
43
+ return $parser->transform($text);
44
+ }
45
+
46
+ ### Configuration Variables ###
47
+
48
+ # Change to ">" for HTML output.
49
+ public $empty_element_suffix = " />";
50
+ public $tab_width = 4;
51
+
52
+ # Change to `true` to disallow markup or entities.
53
+ public $no_markup = FALSE;
54
+ public $no_entities = FALSE;
55
+
56
+ # Predefined urls and titles for reference links and images.
57
+ public $predef_urls = array();
58
+ public $predef_titles = array();
59
+
60
+ ### Parser Implementation ###
61
+
62
+ # Regex to match balanced [brackets].
63
+ # Needed to insert a maximum bracked depth while converting to PHP.
64
+ protected $nested_brackets_depth = 6;
65
+ protected $nested_brackets_re;
66
+
67
+ protected $nested_url_parenthesis_depth = 4;
68
+ protected $nested_url_parenthesis_re;
69
+
70
+ # Table of hash values for escaped characters:
71
+ protected $escape_chars = '\`*_{}[]()>#+-.!';
72
+ protected $escape_chars_re;
73
+
74
+ public function __construct()
75
+ {
76
+ #
77
+ # Constructor function. Initialize appropriate member variables.
78
+ #
79
+ $this->_initDetab();
80
+ $this->prepareItalicsAndBold();
81
+
82
+ $this->nested_brackets_re =
83
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth) .
84
+ str_repeat('\])*', $this->nested_brackets_depth);
85
+
86
+ $this->nested_url_parenthesis_re =
87
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth) .
88
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
89
+
90
+ $this->escape_chars_re = '[' . preg_quote($this->escape_chars) . ']';
91
+
92
+ # Sort document, block, and span gamut in ascendent priority order.
93
+ asort($this->document_gamut);
94
+ asort($this->block_gamut);
95
+ asort($this->span_gamut);
96
+ }
97
+
98
+ # Internal hashes used during transformation.
99
+ protected $urls = array();
100
+ protected $titles = array();
101
+ protected $html_hashes = array();
102
+
103
+ # Status flag to avoid invalid nesting.
104
+ protected $in_anchor = FALSE;
105
+
106
+ protected function setup()
107
+ {
108
+ #
109
+ # Called before the transformation process starts to setup parser
110
+ # states.
111
+ #
112
+ # Clear global hashes.
113
+ $this->urls = $this->predef_urls;
114
+ $this->titles = $this->predef_titles;
115
+ $this->html_hashes = array();
116
+
117
+ $this->in_anchor = FALSE;
118
+ }
119
+
120
+ protected function teardown()
121
+ {
122
+ #
123
+ # Called after the transformation process to clear any variable
124
+ # which may be taking up memory unnecessarly.
125
+ #
126
+ $this->urls = array();
127
+ $this->titles = array();
128
+ $this->html_hashes = array();
129
+ }
130
+
131
+ public function transform($text)
132
+ {
133
+ #
134
+ # Main function. Performs some preprocessing on the input text
135
+ # and pass it through the document gamut.
136
+ #
137
+ $this->setup();
138
+
139
+ # Remove UTF-8 BOM and marker character in input, if present.
140
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
141
+
142
+ # Standardize line endings:
143
+ # DOS to Unix and Mac to Unix
144
+ $text = preg_replace('{\r\n?}', "\n", $text);
145
+
146
+ # Make sure $text ends with a couple of newlines:
147
+ $text .= "\n\n";
148
+
149
+ # Convert all tabs to spaces.
150
+ $text = $this->detab($text);
151
+
152
+ # Turn block-level HTML blocks into hash entries
153
+ $text = $this->hashHTMLBlocks($text);
154
+
155
+ # Strip any lines consisting only of spaces and tabs.
156
+ # This makes subsequent regexen easier to write, because we can
157
+ # match consecutive blank lines with /\n+/ instead of something
158
+ # contorted like /[ ]*\n+/ .
159
+ $text = preg_replace('/^[ ]+$/m', '', $text);
160
+
161
+ # Run document gamut methods.
162
+ foreach ($this->document_gamut as $method => $priority) {
163
+ $text = $this->$method($text);
164
+ }
165
+
166
+ $this->teardown();
167
+
168
+ return $text . "\n";
169
+ }
170
+
171
+ protected $document_gamut = array(
172
+ # Strip link definitions, store in hashes.
173
+ "stripLinkDefinitions" => 20,
174
+
175
+ "runBasicBlockGamut" => 30,
176
+ );
177
+
178
+ protected function stripLinkDefinitions($text)
179
+ {
180
+ #
181
+ # Strips link definitions from text, stores the URLs and titles in
182
+ # hash references.
183
+ #
184
+ $less_than_tab = $this->tab_width - 1;
185
+
186
+ # Link defs are in the form: ^[id]: url "optional title"
187
+ $text = preg_replace_callback('{
188
+ ^[ ]{0,' . $less_than_tab . '}\[(.+)\][ ]?: # id = $1
189
+ [ ]*
190
+ \n? # maybe *one* newline
191
+ [ ]*
192
+ (?:
193
+ <(.+?)> # url = $2
194
+ |
195
+ (\S+?) # url = $3
196
+ )
197
+ [ ]*
198
+ \n? # maybe one newline
199
+ [ ]*
200
+ (?:
201
+ (?<=\s) # lookbehind for whitespace
202
+ ["(]
203
+ (.*?) # title = $4
204
+ [")]
205
+ [ ]*
206
+ )? # title is optional
207
+ (?:\n+|\Z)
208
+ }xm',
209
+ array(&$this, '_stripLinkDefinitions_callback'),
210
+ $text);
211
+ return $text;
212
+ }
213
+
214
+ protected function _stripLinkDefinitions_callback($matches)
215
+ {
216
+ $link_id = strtolower($matches[1]);
217
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
218
+ $this->urls[$link_id] = $url;
219
+ $this->titles[$link_id] =& $matches[4];
220
+ return ''; # String that will replace the block
221
+ }
222
+
223
+ protected function hashHTMLBlocks($text)
224
+ {
225
+ if ($this->no_markup) return $text;
226
+
227
+ $less_than_tab = $this->tab_width - 1;
228
+
229
+ # Hashify HTML blocks:
230
+ # We only want to do this for block-level HTML tags, such as headers,
231
+ # lists, and tables. That's because we still want to wrap <p>s around
232
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
233
+ # phrase emphasis, and spans. The list of tags we're looking for is
234
+ # hard-coded:
235
+ #
236
+ # * List "a" is made of tags which can be both inline or block-level.
237
+ # These will be treated block-level when the start tag is alone on
238
+ # its line, otherwise they're not matched here and will be taken as
239
+ # inline later.
240
+ # * List "b" is made of tags which are always block-level;
241
+ #
242
+ $block_tags_a_re = 'ins|del';
243
+ $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|' .
244
+ 'script|noscript|form|fieldset|iframe|math|svg|' .
245
+ 'article|section|nav|aside|hgroup|header|footer|' .
246
+ 'figure';
247
+
248
+ # Regular expression for the content of a block tag.
249
+ $nested_tags_level = 4;
250
+ $attr = '
251
+ (?> # optional tag attributes
252
+ \s # starts with whitespace
253
+ (?>
254
+ [^>"/]+ # text outside quotes
255
+ |
256
+ /+(?!>) # slash not followed by ">"
257
+ |
258
+ "[^"]*" # text inside double quotes (tolerate ">")
259
+ |
260
+ \'[^\']*\' # text inside single quotes (tolerate ">")
261
+ )*
262
+ )?
263
+ ';
264
+ $content =
265
+ str_repeat('
266
+ (?>
267
+ [^<]+ # content without tag
268
+ |
269
+ <\2 # nested opening tag
270
+ ' . $attr . ' # attributes
271
+ (?>
272
+ />
273
+ |
274
+ >', $nested_tags_level) . # end of opening tag
275
+ '.*?' . # last level nested tag content
276
+ str_repeat('
277
+ </\2\s*> # closing nested tag
278
+ )
279
+ |
280
+ <(?!/\2\s*> # other tags with a different name
281
+ )
282
+ )*',
283
+ $nested_tags_level);
284
+ $content2 = str_replace('\2', '\3', $content);
285
+
286
+ # First, look for nested blocks, e.g.:
287
+ # <div>
288
+ # <div>
289
+ # tags for inner block must be indented.
290
+ # </div>
291
+ # </div>
292
+ #
293
+ # The outermost tags must start at the left margin for this to match, and
294
+ # the inner nested divs must be indented.
295
+ # We need to do this before the next, more liberal match, because the next
296
+ # match will start at the first `<div>` and stop at the first `</div>`.
297
+ $text = preg_replace_callback('{(?>
298
+ (?>
299
+ (?<=\n\n) # Starting after a blank line
300
+ | # or
301
+ \A\n? # the beginning of the doc
302
+ )
303
+ ( # save in $1
304
+
305
+ # Match from `\n<tag>` to `</tag>\n`, handling nested tags
306
+ # in between.
307
+
308
+ [ ]{0,' . $less_than_tab . '}
309
+ <(' . $block_tags_b_re . ')# start tag = $2
310
+ ' . $attr . '> # attributes followed by > and \n
311
+ ' . $content . ' # content, support nesting
312
+ </\2> # the matching end tag
313
+ [ ]* # trailing spaces/tabs
314
+ (?=\n+|\Z) # followed by a newline or end of document
315
+
316
+ | # Special version for tags of group a.
317
+
318
+ [ ]{0,' . $less_than_tab . '}
319
+ <(' . $block_tags_a_re . ')# start tag = $3
320
+ ' . $attr . '>[ ]*\n # attributes followed by >
321
+ ' . $content2 . ' # content, support nesting
322
+ </\3> # the matching end tag
323
+ [ ]* # trailing spaces/tabs
324
+ (?=\n+|\Z) # followed by a newline or end of document
325
+
326
+ | # Special case just for <hr />. It was easier to make a special
327
+ # case than to make the other regex more complicated.
328
+
329
+ [ ]{0,' . $less_than_tab . '}
330
+ <(hr) # start tag = $2
331
+ ' . $attr . ' # attributes
332
+ /?> # the matching end tag
333
+ [ ]*
334
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
335
+
336
+ | # Special case for standalone HTML comments:
337
+
338
+ [ ]{0,' . $less_than_tab . '}
339
+ (?s:
340
+ <!-- .*? -->
341
+ )
342
+ [ ]*
343
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
344
+
345
+ | # PHP and ASP-style processor instructions (<? and <%)
346
+
347
+ [ ]{0,' . $less_than_tab . '}
348
+ (?s:
349
+ <([?%]) # $2
350
+ .*?
351
+ \2>
352
+ )
353
+ [ ]*
354
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
355
+
356
+ )
357
+ )}Sxmi',
358
+ array(&$this, '_hashHTMLBlocks_callback'),
359
+ $text);
360
+
361
+ return $text;
362
+ }
363
+
364
+ protected function _hashHTMLBlocks_callback($matches)
365
+ {
366
+ $text = $matches[1];
367
+ $key = $this->hashBlock($text);
368
+ return "\n\n$key\n\n";
369
+ }
370
+
371
+ protected function hashPart($text, $boundary = 'X')
372
+ {
373
+ #
374
+ # Called whenever a tag must be hashed when a function insert an atomic
375
+ # element in the text stream. Passing $text to through this function gives
376
+ # a unique text-token which will be reverted back when calling unhash.
377
+ #
378
+ # The $boundary argument specify what character should be used to surround
379
+ # the token. By convension, "B" is used for block elements that needs not
380
+ # to be wrapped into paragraph tags at the end, ":" is used for elements
381
+ # that are word separators and "X" is used in the general case.
382
+ #
383
+ # Swap back any tag hash found in $text so we do not have to `unhash`
384
+ # multiple times at the end.
385
+ $text = $this->unhash($text);
386
+
387
+ # Then hash the block.
388
+ static $i = 0;
389
+ $key = "$boundary\x1A" . ++$i . $boundary;
390
+ $this->html_hashes[$key] = $text;
391
+ return $key; # String that will replace the tag.
392
+ }
393
+
394
+ protected function hashBlock($text)
395
+ {
396
+ #
397
+ # Shortcut function for hashPart with block-level boundaries.
398
+ #
399
+ return $this->hashPart($text, 'B');
400
+ }
401
+
402
+ protected $block_gamut = array(
403
+ #
404
+ # These are all the transformations that form block-level
405
+ # tags like paragraphs, headers, and list items.
406
+ #
407
+ "doHeaders" => 10,
408
+ "doHorizontalRules" => 20,
409
+
410
+ "doLists" => 40,
411
+ "doCodeBlocks" => 50,
412
+ "doBlockQuotes" => 60,
413
+ );
414
+
415
+ protected function runBlockGamut($text)
416
+ {
417
+ #
418
+ # Run block gamut tranformations.
419
+ #
420
+ # We need to escape raw HTML in Markdown source before doing anything
421
+ # else. This need to be done for each block, and not only at the
422
+ # begining in the Markdown function since hashed blocks can be part of
423
+ # list items and could have been indented. Indented blocks would have
424
+ # been seen as a code block in a previous pass of hashHTMLBlocks.
425
+ $text = $this->hashHTMLBlocks($text);
426
+
427
+ return $this->runBasicBlockGamut($text);
428
+ }
429
+
430
+ protected function runBasicBlockGamut($text)
431
+ {
432
+ #
433
+ # Run block gamut tranformations, without hashing HTML blocks. This is
434
+ # useful when HTML blocks are known to be already hashed, like in the first
435
+ # whole-document pass.
436
+ #
437
+ foreach ($this->block_gamut as $method => $priority) {
438
+ $text = $this->$method($text);
439
+ }
440
+
441
+ # Finally form paragraph and restore hashed blocks.
442
+ $text = $this->formParagraphs($text);
443
+
444
+ return $text;
445
+ }
446
+
447
+ protected function doHorizontalRules($text)
448
+ {
449
+ # Do Horizontal Rules:
450
+ return preg_replace(
451
+ '{
452
+ ^[ ]{0,3} # Leading space
453
+ ([-*_]) # $1: First marker
454
+ (?> # Repeated marker group
455
+ [ ]{0,2} # Zero, one, or two spaces.
456
+ \1 # Marker character
457
+ ){2,} # Group repeated at least twice
458
+ [ ]* # Tailing spaces
459
+ $ # End of line.
460
+ }mx',
461
+ "\n" . $this->hashBlock("<hr$this->empty_element_suffix") . "\n",
462
+ $text);
463
+ }
464
+
465
+ protected $span_gamut = array(
466
+ #
467
+ # These are all the transformations that occur *within* block-level
468
+ # tags like paragraphs, headers, and list items.
469
+ #
470
+ # Process character escapes, code spans, and inline HTML
471
+ # in one shot.
472
+ "parseSpan" => -30,
473
+
474
+ # Process anchor and image tags. Images must come first,
475
+ # because ![foo][f] looks like an anchor.
476
+ "doImages" => 10,
477
+ "doAnchors" => 20,
478
+
479
+ # Make links out of things like `<http://example.com/>`
480
+ # Must come after doAnchors, because you can use < and >
481
+ # delimiters in inline links like [this](<url>).
482
+ "doAutoLinks" => 30,
483
+ "encodeAmpsAndAngles" => 40,
484
+
485
+ "doItalicsAndBold" => 50,
486
+ "doHardBreaks" => 60,
487
+ );
488
+
489
+ protected function runSpanGamut($text)
490
+ {
491
+ #
492
+ # Run span gamut tranformations.
493
+ #
494
+ foreach ($this->span_gamut as $method => $priority) {
495
+ $text = $this->$method($text);
496
+ }
497
+
498
+ return $text;
499
+ }
500
+
501
+ protected function doHardBreaks($text)
502
+ {
503
+ # Do hard breaks:
504
+ return preg_replace_callback('/ {2,}\n/',
505
+ array(&$this, '_doHardBreaks_callback'), $text);
506
+ }
507
+
508
+ protected function _doHardBreaks_callback($matches)
509
+ {
510
+ return $this->hashPart("<br$this->empty_element_suffix\n");
511
+ }
512
+
513
+ protected function doAnchors($text)
514
+ {
515
+ #
516
+ # Turn Markdown link shortcuts into XHTML <a> tags.
517
+ #
518
+ if ($this->in_anchor) return $text;
519
+ $this->in_anchor = TRUE;
520
+
521
+ #
522
+ # First, handle reference-style links: [link text] [id]
523
+ #
524
+ $text = preg_replace_callback('{
525
+ ( # wrap whole match in $1
526
+ \[
527
+ (' . $this->nested_brackets_re . ') # link text = $2
528
+ \]
529
+
530
+ [ ]? # one optional space
531
+ (?:\n[ ]*)? # one optional newline followed by spaces
532
+
533
+ \[
534
+ (.*?) # id = $3
535
+ \]
536
+ )
537
+ }xs',
538
+ array(&$this, '_doAnchors_reference_callback'), $text);
539
+
540
+ #
541
+ # Next, inline-style links: [link text](url "optional title")
542
+ #
543
+ $text = preg_replace_callback('{
544
+ ( # wrap whole match in $1
545
+ \[
546
+ (' . $this->nested_brackets_re . ') # link text = $2
547
+ \]
548
+ \( # literal paren
549
+ [ \n]*
550
+ (?:
551
+ <(.+?)> # href = $3
552
+ |
553
+ (' . $this->nested_url_parenthesis_re . ') # href = $4
554
+ )
555
+ [ \n]*
556
+ ( # $5
557
+ ([\'"]) # quote char = $6
558
+ (.*?) # Title = $7
559
+ \6 # matching quote
560
+ [ \n]* # ignore any spaces/tabs between closing quote and )
561
+ )? # title is optional
562
+ \)
563
+ )
564
+ }xs',
565
+ array(&$this, '_doAnchors_inline_callback'), $text);
566
+
567
+ #
568
+ # Last, handle reference-style shortcuts: [link text]
569
+ # These must come last in case you've also got [link text][1]
570
+ # or [link text](/foo)
571
+ #
572
+ $text = preg_replace_callback('{
573
+ ( # wrap whole match in $1
574
+ \[
575
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
576
+ \]
577
+ )
578
+ }xs',
579
+ array(&$this, '_doAnchors_reference_callback'), $text);
580
+
581
+ $this->in_anchor = FALSE;
582
+ return $text;
583
+ }
584
+
585
+ protected function _doAnchors_reference_callback($matches)
586
+ {
587
+ $whole_match = $matches[1];
588
+ $link_text = $matches[2];
589
+ $link_id =& $matches[3];
590
+
591
+ if ($link_id == "") {
592
+ # for shortcut links like [this][] or [this].
593
+ $link_id = $link_text;
594
+ }
595
+
596
+ # lower-case and turn embedded newlines into spaces
597
+ $link_id = strtolower($link_id);
598
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
599
+
600
+ if (isset($this->urls[$link_id])) {
601
+ $url = $this->urls[$link_id];
602
+ $url = $this->encodeAttribute($url);
603
+
604
+ $result = "<a href=\"$url\"";
605
+ if (isset($this->titles[$link_id])) {
606
+ $title = $this->titles[$link_id];
607
+ $title = $this->encodeAttribute($title);
608
+ $result .= " title=\"$title\"";
609
+ }
610
+
611
+ $link_text = $this->runSpanGamut($link_text);
612
+ $result .= ">$link_text</a>";
613
+ $result = $this->hashPart($result);
614
+ } else {
615
+ $result = $whole_match;
616
+ }
617
+ return $result;
618
+ }
619
+
620
+ protected function _doAnchors_inline_callback($matches)
621
+ {
622
+ $whole_match = $matches[1];
623
+ $link_text = $this->runSpanGamut($matches[2]);
624
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
625
+ $title =& $matches[7];
626
+
627
+ $url = $this->encodeAttribute($url);
628
+
629
+ $result = "<a href=\"$url\"";
630
+ if (isset($title)) {
631
+ $title = $this->encodeAttribute($title);
632
+ $result .= " title=\"$title\"";
633
+ }
634
+
635
+ $link_text = $this->runSpanGamut($link_text);
636
+ $result .= ">$link_text</a>";
637
+
638
+ return $this->hashPart($result);
639
+ }
640
+
641
+ protected function doImages($text)
642
+ {
643
+ #
644
+ # Turn Markdown image shortcuts into <img> tags.
645
+ #
646
+ #
647
+ # First, handle reference-style labeled images: ![alt text][id]
648
+ #
649
+ $text = preg_replace_callback('{
650
+ ( # wrap whole match in $1
651
+ !\[
652
+ (' . $this->nested_brackets_re . ') # alt text = $2
653
+ \]
654
+
655
+ [ ]? # one optional space
656
+ (?:\n[ ]*)? # one optional newline followed by spaces
657
+
658
+ \[
659
+ (.*?) # id = $3
660
+ \]
661
+
662
+ )
663
+ }xs',
664
+ array(&$this, '_doImages_reference_callback'), $text);
665
+
666
+ #
667
+ # Next, handle inline images: ![alt text](url "optional title")
668
+ # Don't forget: encode * and _
669
+ #
670
+ $text = preg_replace_callback('{
671
+ ( # wrap whole match in $1
672
+ !\[
673
+ (' . $this->nested_brackets_re . ') # alt text = $2
674
+ \]
675
+ \s? # One optional whitespace character
676
+ \( # literal paren
677
+ [ \n]*
678
+ (?:
679
+ <(\S*)> # src url = $3
680
+ |
681
+ (' . $this->nested_url_parenthesis_re . ') # src url = $4
682
+ )
683
+ [ \n]*
684
+ ( # $5
685
+ ([\'"]) # quote char = $6
686
+ (.*?) # title = $7
687
+ \6 # matching quote
688
+ [ \n]*
689
+ )? # title is optional
690
+ \)
691
+ )
692
+ }xs',
693
+ array(&$this, '_doImages_inline_callback'), $text);
694
+
695
+ return $text;
696
+ }
697
+
698
+ protected function _doImages_reference_callback($matches)
699
+ {
700
+ $whole_match = $matches[1];
701
+ $alt_text = $matches[2];
702
+ $link_id = strtolower($matches[3]);
703
+
704
+ if ($link_id == "") {
705
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
706
+ }
707
+
708
+ $alt_text = $this->encodeAttribute($alt_text);
709
+ if (isset($this->urls[$link_id])) {
710
+ $url = $this->encodeAttribute($this->urls[$link_id]);
711
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
712
+ if (isset($this->titles[$link_id])) {
713
+ $title = $this->titles[$link_id];
714
+ $title = $this->encodeAttribute($title);
715
+ $result .= " title=\"$title\"";
716
+ }
717
+ $result .= $this->empty_element_suffix;
718
+ $result = $this->hashPart($result);
719
+ } else {
720
+ # If there's no such link ID, leave intact:
721
+ $result = $whole_match;
722
+ }
723
+
724
+ return $result;
725
+ }
726
+
727
+ protected function _doImages_inline_callback($matches)
728
+ {
729
+ $whole_match = $matches[1];
730
+ $alt_text = $matches[2];
731
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
732
+ $title =& $matches[7];
733
+
734
+ $alt_text = $this->encodeAttribute($alt_text);
735
+ $url = $this->encodeAttribute($url);
736
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
737
+ if (isset($title)) {
738
+ $title = $this->encodeAttribute($title);
739
+ $result .= " title=\"$title\""; # $title already quoted
740
+ }
741
+ $result .= $this->empty_element_suffix;
742
+
743
+ return $this->hashPart($result);
744
+ }
745
+
746
+ protected function doHeaders($text)
747
+ {
748
+ # Setext-style headers:
749
+ # Header 1
750
+ # ========
751
+ #
752
+ # Header 2
753
+ # --------
754
+ #
755
+ $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
756
+ array(&$this, '_doHeaders_callback_setext'), $text);
757
+
758
+ # atx-style headers:
759
+ # # Header 1
760
+ # ## Header 2
761
+ # ## Header 2 with closing hashes ##
762
+ # ...
763
+ # ###### Header 6
764
+ #
765
+ $text = preg_replace_callback('{
766
+ ^(\#{1,6}) # $1 = string of #\'s
767
+ [ ]*
768
+ (.+?) # $2 = Header text
769
+ [ ]*
770
+ \#* # optional closing #\'s (not counted)
771
+ \n+
772
+ }xm',
773
+ array(&$this, '_doHeaders_callback_atx'), $text);
774
+
775
+ return $text;
776
+ }
777
+
778
+ protected function _doHeaders_callback_setext($matches)
779
+ {
780
+ # Terrible hack to check we haven't found an empty list item.
781
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
782
+ return $matches[0];
783
+
784
+ $level = $matches[2]{0} == '=' ? 1 : 2;
785
+ $block = "<h$level>" . $this->runSpanGamut($matches[1]) . "</h$level>";
786
+ return "\n" . $this->hashBlock($block) . "\n\n";
787
+ }
788
+
789
+ protected function _doHeaders_callback_atx($matches)
790
+ {
791
+ $level = strlen($matches[1]);
792
+ $block = "<h$level>" . $this->runSpanGamut($matches[2]) . "</h$level>";
793
+ return "\n" . $this->hashBlock($block) . "\n\n";
794
+ }
795
+
796
+ protected function doLists($text)
797
+ {
798
+ #
799
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
800
+ #
801
+ $less_than_tab = $this->tab_width - 1;
802
+
803
+ # Re-usable patterns to match list item bullets and number markers:
804
+ $marker_ul_re = '[*+-]';
805
+ $marker_ol_re = '\d+[\.]';
806
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
807
+
808
+ $markers_relist = array(
809
+ $marker_ul_re => $marker_ol_re,
810
+ $marker_ol_re => $marker_ul_re,
811
+ );
812
+
813
+ foreach ($markers_relist as $marker_re => $other_marker_re) {
814
+ # Re-usable pattern to match any entirel ul or ol list:
815
+ $whole_list_re = '
816
+ ( # $1 = whole list
817
+ ( # $2
818
+ ([ ]{0,' . $less_than_tab . '}) # $3 = number of spaces
819
+ (' . $marker_re . ') # $4 = first list item marker
820
+ [ ]+
821
+ )
822
+ (?s:.+?)
823
+ ( # $5
824
+ \z
825
+ |
826
+ \n{2,}
827
+ (?=\S)
828
+ (?! # Negative lookahead for another list item marker
829
+ [ ]*
830
+ ' . $marker_re . '[ ]+
831
+ )
832
+ |
833
+ (?= # Lookahead for another kind of list
834
+ \n
835
+ \3 # Must have the same indentation
836
+ ' . $other_marker_re . '[ ]+
837
+ )
838
+ )
839
+ )
840
+ '; // mx
841
+
842
+ # We use a different prefix before nested lists than top-level lists.
843
+ # See extended comment in _ProcessListItems().
844
+
845
+ if ($this->list_level) {
846
+ $text = preg_replace_callback('{
847
+ ^
848
+ ' . $whole_list_re . '
849
+ }mx',
850
+ array(&$this, '_doLists_callback'), $text);
851
+ } else {
852
+ $text = preg_replace_callback('{
853
+ (?:(?<=\n)\n|\A\n?) # Must eat the newline
854
+ ' . $whole_list_re . '
855
+ }mx',
856
+ array(&$this, '_doLists_callback'), $text);
857
+ }
858
+ }
859
+
860
+ return $text;
861
+ }
862
+
863
+ protected function _doLists_callback($matches)
864
+ {
865
+ # Re-usable patterns to match list item bullets and number markers:
866
+ $marker_ul_re = '[*+-]';
867
+ $marker_ol_re = '\d+[\.]';
868
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
869
+
870
+ $list = $matches[1];
871
+ $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
872
+
873
+ $marker_any_re = ($list_type == "ul" ? $marker_ul_re : $marker_ol_re);
874
+
875
+ $list .= "\n";
876
+ $result = $this->processListItems($list, $marker_any_re);
877
+
878
+ $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
879
+ return "\n" . $result . "\n\n";
880
+ }
881
+
882
+ protected $list_level = 0;
883
+
884
+ protected function processListItems($list_str, $marker_any_re)
885
+ {
886
+ #
887
+ # Process the contents of a single ordered or unordered list, splitting it
888
+ # into individual list items.
889
+ #
890
+ # The $this->list_level global keeps track of when we're inside a list.
891
+ # Each time we enter a list, we increment it; when we leave a list,
892
+ # we decrement. If it's zero, we're not in a list anymore.
893
+ #
894
+ # We do this because when we're not inside a list, we want to treat
895
+ # something like this:
896
+ #
897
+ # I recommend upgrading to version
898
+ # 8. Oops, now this line is treated
899
+ # as a sub-list.
900
+ #
901
+ # As a single paragraph, despite the fact that the second line starts
902
+ # with a digit-period-space sequence.
903
+ #
904
+ # Whereas when we're inside a list (or sub-list), that line will be
905
+ # treated as the start of a sub-list. What a kludge, huh? This is
906
+ # an aspect of Markdown's syntax that's hard to parse perfectly
907
+ # without resorting to mind-reading. Perhaps the solution is to
908
+ # change the syntax rules such that sub-lists must start with a
909
+ # starting cardinal number; e.g. "1." or "a.".
910
+
911
+ $this->list_level++;
912
+
913
+ # trim trailing blank lines:
914
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
915
+
916
+ $list_str = preg_replace_callback('{
917
+ (\n)? # leading line = $1
918
+ (^[ ]*) # leading whitespace = $2
919
+ (' . $marker_any_re . ' # list marker and space = $3
920
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
921
+ )
922
+ ((?s:.*?)) # list item text = $4
923
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
924
+ (?= \n* (\z | \2 (' . $marker_any_re . ') (?:[ ]+|(?=\n))))
925
+ }xm',
926
+ array(&$this, '_processListItems_callback'), $list_str);
927
+
928
+ $this->list_level--;
929
+ return $list_str;
930
+ }
931
+
932
+ protected function _processListItems_callback($matches)
933
+ {
934
+ $item = $matches[4];
935
+ $leading_line =& $matches[1];
936
+ $leading_space =& $matches[2];
937
+ $marker_space = $matches[3];
938
+ $tailing_blank_line =& $matches[5];
939
+
940
+ if ($leading_line || $tailing_blank_line ||
941
+ preg_match('/\n{2,}/', $item)
942
+ ) {
943
+ # Replace marker with the appropriate whitespace indentation
944
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
945
+ $item = $this->runBlockGamut($this->outdent($item) . "\n");
946
+ } else {
947
+ # Recursion for sub-lists:
948
+ $item = $this->doLists($this->outdent($item));
949
+ $item = preg_replace('/\n+$/', '', $item);
950
+ $item = $this->runSpanGamut($item);
951
+ }
952
+
953
+ return "<li>" . $item . "</li>\n";
954
+ }
955
+
956
+ protected function doCodeBlocks($text)
957
+ {
958
+ #
959
+ # Process Markdown `<pre><code>` blocks.
960
+ #
961
+ $text = preg_replace_callback('{
962
+ (?:\n\n|\A\n?)
963
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
964
+ (?>
965
+ [ ]{' . $this->tab_width . '} # Lines must start with a tab or a tab-width of spaces
966
+ .*\n+
967
+ )+
968
+ )
969
+ ((?=^[ ]{0,' . $this->tab_width . '}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
970
+ }xm',
971
+ array(&$this, '_doCodeBlocks_callback'), $text);
972
+
973
+ return $text;
974
+ }
975
+
976
+ protected function _doCodeBlocks_callback($matches)
977
+ {
978
+ $codeblock = $matches[1];
979
+
980
+ $codeblock = $this->outdent($codeblock);
981
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
982
+
983
+ # trim leading newlines and trailing newlines
984
+ $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
985
+
986
+ $codeblock = "<pre><code>$codeblock\n</code></pre>";
987
+ return "\n\n" . $this->hashBlock($codeblock) . "\n\n";
988
+ }
989
+
990
+ protected function makeCodeSpan($code)
991
+ {
992
+ #
993
+ # Create a code span markup for $code. Called from handleSpanToken.
994
+ #
995
+ $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
996
+ return $this->hashPart("<code>$code</code>");
997
+ }
998
+
999
+ protected $em_relist = array(
1000
+ '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S|$)(?![\.,:;]\s)',
1001
+ '*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
1002
+ '_' => '(?<=\S|^)(?<!_)_(?!_)',
1003
+ );
1004
+ protected $strong_relist = array(
1005
+ '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S|$)(?![\.,:;]\s)',
1006
+ '**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
1007
+ '__' => '(?<=\S|^)(?<!_)__(?!_)',
1008
+ );
1009
+ protected $em_strong_relist = array(
1010
+ '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S|$)(?![\.,:;]\s)',
1011
+ '***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
1012
+ '___' => '(?<=\S|^)(?<!_)___(?!_)',
1013
+ );
1014
+ protected $em_strong_prepared_relist;
1015
+
1016
+ protected function prepareItalicsAndBold()
1017
+ {
1018
+ #
1019
+ # Prepare regular expressions for searching emphasis tokens in any
1020
+ # context.
1021
+ #
1022
+ foreach ($this->em_relist as $em => $em_re) {
1023
+ foreach ($this->strong_relist as $strong => $strong_re) {
1024
+ # Construct list of allowed token expressions.
1025
+ $token_relist = array();
1026
+ if (isset($this->em_strong_relist["$em$strong"])) {
1027
+ $token_relist[] = $this->em_strong_relist["$em$strong"];
1028
+ }
1029
+ $token_relist[] = $em_re;
1030
+ $token_relist[] = $strong_re;
1031
+
1032
+ # Construct master expression from list.
1033
+ $token_re = '{(' . implode('|', $token_relist) . ')}';
1034
+ $this->em_strong_prepared_relist["$em$strong"] = $token_re;
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ protected function doItalicsAndBold($text)
1040
+ {
1041
+ $token_stack = array('');
1042
+ $text_stack = array('');
1043
+ $em = '';
1044
+ $strong = '';
1045
+ $tree_char_em = FALSE;
1046
+
1047
+ while (1) {
1048
+ #
1049
+ # Get prepared regular expression for seraching emphasis tokens
1050
+ # in current context.
1051
+ #
1052
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
1053
+
1054
+ #
1055
+ # Each loop iteration search for the next emphasis token.
1056
+ # Each token is then passed to handleSpanToken.
1057
+ #
1058
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
1059
+ $text_stack[0] .= $parts[0];
1060
+ $token =& $parts[1];
1061
+ $text =& $parts[2];
1062
+
1063
+ if (empty($token)) {
1064
+ # Reached end of text span: empty stack without emitting.
1065
+ # any more emphasis.
1066
+ while ($token_stack[0]) {
1067
+ $text_stack[1] .= array_shift($token_stack);
1068
+ $text_stack[0] .= array_shift($text_stack);
1069
+ }
1070
+ break;
1071
+ }
1072
+
1073
+ $token_len = strlen($token);
1074
+ if ($tree_char_em) {
1075
+ # Reached closing marker while inside a three-char emphasis.
1076
+ if ($token_len == 3) {
1077
+ # Three-char closing marker, close em and strong.
1078
+ array_shift($token_stack);
1079
+ $span = array_shift($text_stack);
1080
+ $span = $this->runSpanGamut($span);
1081
+ $span = "<strong><em>$span</em></strong>";
1082
+ $text_stack[0] .= $this->hashPart($span);
1083
+ $em = '';
1084
+ $strong = '';
1085
+ } else {
1086
+ # Other closing marker: close one em or strong and
1087
+ # change current token state to match the other
1088
+ $token_stack[0] = str_repeat($token{0}, 3 - $token_len);
1089
+ $tag = $token_len == 2 ? "strong" : "em";
1090
+ $span = $text_stack[0];
1091
+ $span = $this->runSpanGamut($span);
1092
+ $span = "<$tag>$span</$tag>";
1093
+ $text_stack[0] = $this->hashPart($span);
1094
+ $$tag = ''; # $$tag stands for $em or $strong
1095
+ }
1096
+ $tree_char_em = FALSE;
1097
+ } else if ($token_len == 3) {
1098
+ if ($em) {
1099
+ # Reached closing marker for both em and strong.
1100
+ # Closing strong marker:
1101
+ for ($i = 0; $i < 2; ++$i) {
1102
+ $shifted_token = array_shift($token_stack);
1103
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
1104
+ $span = array_shift($text_stack);
1105
+ $span = $this->runSpanGamut($span);
1106
+ $span = "<$tag>$span</$tag>";
1107
+ $text_stack[0] .= $this->hashPart($span);
1108
+ $$tag = ''; # $$tag stands for $em or $strong
1109
+ }
1110
+ } else {
1111
+ # Reached opening three-char emphasis marker. Push on token
1112
+ # stack; will be handled by the special condition above.
1113
+ $em = $token{0};
1114
+ $strong = "$em$em";
1115
+ array_unshift($token_stack, $token);
1116
+ array_unshift($text_stack, '');
1117
+ $tree_char_em = TRUE;
1118
+ }
1119
+ } else if ($token_len == 2) {
1120
+ if ($strong) {
1121
+ # Unwind any dangling emphasis marker:
1122
+ if (strlen($token_stack[0]) == 1) {
1123
+ $text_stack[1] .= array_shift($token_stack);
1124
+ $text_stack[0] .= array_shift($text_stack);
1125
+ }
1126
+ # Closing strong marker:
1127
+ array_shift($token_stack);
1128
+ $span = array_shift($text_stack);
1129
+ $span = $this->runSpanGamut($span);
1130
+ $span = "<strong>$span</strong>";
1131
+ $text_stack[0] .= $this->hashPart($span);
1132
+ $strong = '';
1133
+ } else {
1134
+ array_unshift($token_stack, $token);
1135
+ array_unshift($text_stack, '');
1136
+ $strong = $token;
1137
+ }
1138
+ } else {
1139
+ # Here $token_len == 1
1140
+ if ($em) {
1141
+ if (strlen($token_stack[0]) == 1) {
1142
+ # Closing emphasis marker:
1143
+ array_shift($token_stack);
1144
+ $span = array_shift($text_stack);
1145
+ $span = $this->runSpanGamut($span);
1146
+ $span = "<em>$span</em>";
1147
+ $text_stack[0] .= $this->hashPart($span);
1148
+ $em = '';
1149
+ } else {
1150
+ $text_stack[0] .= $token;
1151
+ }
1152
+ } else {
1153
+ array_unshift($token_stack, $token);
1154
+ array_unshift($text_stack, '');
1155
+ $em = $token;
1156
+ }
1157
+ }
1158
+ }
1159
+ return $text_stack[0];
1160
+ }
1161
+
1162
+ protected function doBlockQuotes($text)
1163
+ {
1164
+ $text = preg_replace_callback('/
1165
+ ( # Wrap whole match in $1
1166
+ (?>
1167
+ ^[ ]*>[ ]? # ">" at the start of a line
1168
+ .+\n # rest of the first line
1169
+ (.+\n)* # subsequent consecutive lines
1170
+ \n* # blanks
1171
+ )+
1172
+ )
1173
+ /xm',
1174
+ array(&$this, '_doBlockQuotes_callback'), $text);
1175
+
1176
+ return $text;
1177
+ }
1178
+
1179
+ protected function _doBlockQuotes_callback($matches)
1180
+ {
1181
+ $bq = $matches[1];
1182
+ # trim one level of quoting - trim whitespace-only lines
1183
+ $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
1184
+ $bq = $this->runBlockGamut($bq); # recurse
1185
+
1186
+ $bq = preg_replace('/^/m', " ", $bq);
1187
+ # These leading spaces cause problem with <pre> content,
1188
+ # so we need to fix that:
1189
+ $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
1190
+ array(&$this, '_doBlockQuotes_callback2'), $bq);
1191
+
1192
+ return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n";
1193
+ }
1194
+
1195
+ protected function _doBlockQuotes_callback2($matches)
1196
+ {
1197
+ $pre = $matches[1];
1198
+ $pre = preg_replace('/^ /m', '', $pre);
1199
+ return $pre;
1200
+ }
1201
+
1202
+ protected function formParagraphs($text)
1203
+ {
1204
+ #
1205
+ # Params:
1206
+ # $text - string to process with html <p> tags
1207
+ #
1208
+ # Strip leading and trailing lines:
1209
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
1210
+
1211
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1212
+
1213
+ #
1214
+ # Wrap <p> tags and unhashify HTML blocks
1215
+ #
1216
+ foreach ($grafs as $key => $value) {
1217
+ if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
1218
+ # Is a paragraph.
1219
+ $value = $this->runSpanGamut($value);
1220
+ $value = preg_replace('/^([ ]*)/', "<p>", $value);
1221
+ $value .= "</p>";
1222
+ $grafs[$key] = $this->unhash($value);
1223
+ } else {
1224
+ # Is a block.
1225
+ # Modify elements of @grafs in-place...
1226
+ $graf = $value;
1227
+ $block = $this->html_hashes[$graf];
1228
+ $graf = $block;
1229
+ // if (preg_match('{
1230
+ // \A
1231
+ // ( # $1 = <div> tag
1232
+ // <div \s+
1233
+ // [^>]*
1234
+ // \b
1235
+ // markdown\s*=\s* ([\'"]) # $2 = attr quote char
1236
+ // 1
1237
+ // \2
1238
+ // [^>]*
1239
+ // >
1240
+ // )
1241
+ // ( # $3 = contents
1242
+ // .*
1243
+ // )
1244
+ // (</div>) # $4 = closing tag
1245
+ // \z
1246
+ // }xs', $block, $matches))
1247
+ // {
1248
+ // list(, $div_open, , $div_content, $div_close) = $matches;
1249
+ //
1250
+ // # We can't call Markdown(), because that resets the hash;
1251
+ // # that initialization code should be pulled into its own sub, though.
1252
+ // $div_content = $this->hashHTMLBlocks($div_content);
1253
+ //
1254
+ // # Run document gamut methods on the content.
1255
+ // foreach ($this->document_gamut as $method => $priority) {
1256
+ // $div_content = $this->$method($div_content);
1257
+ // }
1258
+ //
1259
+ // $div_open = preg_replace(
1260
+ // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
1261
+ //
1262
+ // $graf = $div_open . "\n" . $div_content . "\n" . $div_close;
1263
+ // }
1264
+ $grafs[$key] = $graf;
1265
+ }
1266
+ }
1267
+
1268
+ return implode("\n\n", $grafs);
1269
+ }
1270
+
1271
+ protected function encodeAttribute($text)
1272
+ {
1273
+ #
1274
+ # Encode text for a double-quoted HTML attribute. This function
1275
+ # is *not* suitable for attributes enclosed in single quotes.
1276
+ #
1277
+ $text = $this->encodeAmpsAndAngles($text);
1278
+ $text = str_replace('"', '&quot;', $text);
1279
+ return $text;
1280
+ }
1281
+
1282
+ protected function encodeAmpsAndAngles($text)
1283
+ {
1284
+ #
1285
+ # Smart processing for ampersands and angle brackets that need to
1286
+ # be encoded. Valid character entities are left alone unless the
1287
+ # no-entities mode is set.
1288
+ #
1289
+ if ($this->no_entities) {
1290
+ $text = str_replace('&', '&amp;', $text);
1291
+ } else {
1292
+ # Ampersand-encoding based entirely on Nat Irons's Amputator
1293
+ # MT plugin: <http://bumppo.net/projects/amputator/>
1294
+ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
1295
+ '&amp;', $text);;
1296
+ }
1297
+ # Encode remaining <'s
1298
+ $text = str_replace('<', '&lt;', $text);
1299
+
1300
+ return $text;
1301
+ }
1302
+
1303
+ protected function doAutoLinks($text)
1304
+ {
1305
+ $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i',
1306
+ array(&$this, '_doAutoLinks_url_callback'), $text);
1307
+
1308
+ # Email addresses: <address@domain.foo>
1309
+ $text = preg_replace_callback('{
1310
+ <
1311
+ (?:mailto:)?
1312
+ (
1313
+ (?:
1314
+ [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
1315
+ |
1316
+ ".*?"
1317
+ )
1318
+ \@
1319
+ (?:
1320
+ [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
1321
+ |
1322
+ \[[\d.a-fA-F:]+\] # IPv4 & IPv6
1323
+ )
1324
+ )
1325
+ >
1326
+ }xi',
1327
+ array(&$this, '_doAutoLinks_email_callback'), $text);
1328
+
1329
+ return $text;
1330
+ }
1331
+
1332
+ protected function _doAutoLinks_url_callback($matches)
1333
+ {
1334
+ $url = $this->encodeAttribute($matches[1]);
1335
+ $link = "<a href=\"$url\">$url</a>";
1336
+ return $this->hashPart($link);
1337
+ }
1338
+
1339
+ protected function _doAutoLinks_email_callback($matches)
1340
+ {
1341
+ $address = $matches[1];
1342
+ $link = $this->encodeEmailAddress($address);
1343
+ return $this->hashPart($link);
1344
+ }
1345
+
1346
+ protected function encodeEmailAddress($addr)
1347
+ {
1348
+ #
1349
+ # Input: an email address, e.g. "foo@example.com"
1350
+ #
1351
+ # Output: the email address as a mailto link, with each character
1352
+ # of the address encoded as either a decimal or hex entity, in
1353
+ # the hopes of foiling most address harvesting spam bots. E.g.:
1354
+ #
1355
+ # <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
1356
+ # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
1357
+ # &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
1358
+ # &#101;&#46;&#x63;&#111;&#x6d;</a></p>
1359
+ #
1360
+ # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
1361
+ # With some optimizations by Milian Wolff.
1362
+ #
1363
+ $addr = "mailto:" . $addr;
1364
+ $chars = preg_split('/(?<!^)(?!$)/', $addr);
1365
+ $seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed.
1366
+
1367
+ foreach ($chars as $key => $char) {
1368
+ $ord = ord($char);
1369
+ # Ignore non-ascii chars.
1370
+ if ($ord < 128) {
1371
+ $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
1372
+ # roughly 10% raw, 45% hex, 45% dec
1373
+ # '@' *must* be encoded. I insist.
1374
+ if ($r > 90 && $char != '@') /* do nothing */
1375
+ ;
1376
+ else if ($r < 45) $chars[$key] = '&#x' . dechex($ord) . ';';
1377
+ else $chars[$key] = '&#' . $ord . ';';
1378
+ }
1379
+ }
1380
+
1381
+ $addr = implode('', $chars);
1382
+ $text = implode('', array_slice($chars, 7)); # text without `mailto:`
1383
+ $addr = "<a href=\"$addr\">$text</a>";
1384
+
1385
+ return $addr;
1386
+ }
1387
+
1388
+ protected function parseSpan($str)
1389
+ {
1390
+ #
1391
+ # Take the string $str and parse it into tokens, hashing embeded HTML,
1392
+ # escaped characters and handling code spans.
1393
+ #
1394
+ $output = '';
1395
+
1396
+ $span_re = '{
1397
+ (
1398
+ \\\\' . $this->escape_chars_re . '
1399
+ |
1400
+ (?<![`\\\\])
1401
+ `+ # code span marker
1402
+ ' . ($this->no_markup ? '' : '
1403
+ |
1404
+ <!-- .*? --> # comment
1405
+ |
1406
+ <\?.*?\?> | <%.*?%> # processing instruction
1407
+ |
1408
+ <[!$]?[-a-zA-Z0-9:_]+ # regular tags
1409
+ (?>
1410
+ \s
1411
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
1412
+ )?
1413
+ >
1414
+ |
1415
+ <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag
1416
+ |
1417
+ </[-a-zA-Z0-9:_]+\s*> # closing tag
1418
+ ') . '
1419
+ )
1420
+ }xs';
1421
+
1422
+ while (1) {
1423
+ #
1424
+ # Each loop iteration seach for either the next tag, the next
1425
+ # openning code span marker, or the next escaped character.
1426
+ # Each token is then passed to handleSpanToken.
1427
+ #
1428
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
1429
+
1430
+ # Create token from text preceding tag.
1431
+ if ($parts[0] != "") {
1432
+ $output .= $parts[0];
1433
+ }
1434
+
1435
+ # Check if we reach the end.
1436
+ if (isset($parts[1])) {
1437
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
1438
+ $str = $parts[2];
1439
+ } else {
1440
+ break;
1441
+ }
1442
+ }
1443
+
1444
+ return $output;
1445
+ }
1446
+
1447
+ protected function handleSpanToken($token, &$str)
1448
+ {
1449
+ #
1450
+ # Handle $token provided by parseSpan by determining its nature and
1451
+ # returning the corresponding value that should replace it.
1452
+ #
1453
+ switch ($token{0}) {
1454
+ case "\\":
1455
+ return $this->hashPart("&#" . ord($token{1}) . ";");
1456
+ case "`":
1457
+ # Search for end marker in remaining text.
1458
+ if (preg_match('/^(.*?[^`])' . preg_quote($token) . '(?!`)(.*)$/sm',
1459
+ $str, $matches)
1460
+ ) {
1461
+ $str = $matches[2];
1462
+ $codespan = $this->makeCodeSpan($matches[1]);
1463
+ return $this->hashPart($codespan);
1464
+ }
1465
+ return $token; // return as text since no ending marker found.
1466
+ default:
1467
+ return $this->hashPart($token);
1468
+ }
1469
+ }
1470
+
1471
+ protected function outdent($text)
1472
+ {
1473
+ #
1474
+ # Remove one level of line-leading tabs or spaces
1475
+ #
1476
+ return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text);
1477
+ }
1478
+
1479
+ # String length function for detab. `_initDetab` will create a function to
1480
+ # hanlde UTF-8 if the default function does not exist.
1481
+ protected $utf8_strlen = 'mb_strlen';
1482
+
1483
+ protected function detab($text)
1484
+ {
1485
+ #
1486
+ # Replace tabs with the appropriate amount of space.
1487
+ #
1488
+ # For each line we separate the line in blocks delemited by
1489
+ # tab characters. Then we reconstruct every line by adding the
1490
+ # appropriate number of space between each blocks.
1491
+
1492
+ $text = preg_replace_callback('/^.*\t.*$/m',
1493
+ array(&$this, '_detab_callback'), $text);
1494
+
1495
+ return $text;
1496
+ }
1497
+
1498
+ protected function _detab_callback($matches)
1499
+ {
1500
+ $line = $matches[0];
1501
+ $strlen = $this->utf8_strlen; # strlen function for UTF-8.
1502
+
1503
+ # Split in blocks.
1504
+ $blocks = explode("\t", $line);
1505
+ # Add each blocks to the line.
1506
+ $line = $blocks[0];
1507
+ unset($blocks[0]); # Do not add first block twice.
1508
+ foreach ($blocks as $block) {
1509
+ # Calculate amount of space, insert spaces, insert block.
1510
+ $amount = $this->tab_width -
1511
+ $strlen($line, 'UTF-8') % $this->tab_width;
1512
+ $line .= str_repeat(" ", $amount) . $block;
1513
+ }
1514
+ return $line;
1515
+ }
1516
+
1517
+ protected function _initDetab()
1518
+ {
1519
+ #
1520
+ # Check for the availability of the function in the `utf8_strlen` property
1521
+ # (initially `mb_strlen`). If the function is not available, create a
1522
+ # function that will loosely count the number of UTF-8 characters with a
1523
+ # regular expression.
1524
+ #
1525
+ if (function_exists($this->utf8_strlen)) return;
1526
+ $this->utf8_strlen = create_function('$text', 'return preg_match_all(
1527
+ "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
1528
+ $text, $m);');
1529
+ }
1530
+
1531
+ protected function unhash($text)
1532
+ {
1533
+ #
1534
+ # Swap back in all the tags hashed by _HashHTMLBlocks.
1535
+ #
1536
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
1537
+ array(&$this, '_unhash_callback'), $text);
1538
+ }
1539
+
1540
+ protected function _unhash_callback($matches)
1541
+ {
1542
+ return $this->html_hashes[$matches[0]];
1543
+ }
1544
+
1545
+ }
app/code/community/SchumacherFM/Markdown/Model/Michelf/Markdown/Extra.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ #
3
+ # Markdown Extra - A text-to-HTML conversion tool for web writers
4
+ #
5
+ # PHP Markdown Extra
6
+ # Copyright (c) 2004-2013 Michel Fortin
7
+ # <http://michelf.com/projects/php-markdown/>
8
+ #
9
+ # Original Markdown
10
+ # Copyright (c) 2004-2006 John Gruber
11
+ # <http://daringfireball.net/projects/markdown/>
12
+ #
13
+
14
+ #
15
+ # Markdown Extra Parser Class
16
+ #
17
+ # Note: Currently the implementation resides in the temporary class
18
+ # \Michelf\MarkdownExtra_TmpImpl (in the same file as \Michelf\Markdown).
19
+ # This makes it easier to propagate the changes between the three different
20
+ # packaging styles of PHP Markdown. Once this issue is resolved, the
21
+ # _MarkdownExtra_TmpImpl will disappear and this one will contain the code.
22
+ #
23
+
24
+ class SchumacherFM_Markdown_Model_Michelf_Markdown_Extra extends SchumacherFM_Markdown_Model_Michelf_Markdown_TmpImpl
25
+ {
26
+
27
+ ### Parser Implementation ###
28
+
29
+ # Temporarily, the implemenation is in the _MarkdownExtra_TmpImpl class.
30
+ # See note above.
31
+
32
+ }
app/code/community/SchumacherFM/Markdown/Model/Michelf/Markdown/TmpImpl.php ADDED
@@ -0,0 +1,1604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ #
4
+ # Temporary Markdown Extra Parser Implementation Class
5
+ #
6
+ # NOTE: DON'T USE THIS CLASS
7
+ # Currently the implementation of of Extra resides here in this temporary class.
8
+ # This makes it easier to propagate the changes between the three different
9
+ # packaging styles of PHP Markdown. When this issue is resolved, this
10
+ # MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra
11
+ # will contain the code. So please use \Michelf\MarkdownExtra and ignore this
12
+ # one.
13
+ #
14
+
15
+ class SchumacherFM_Markdown_Model_Michelf_Markdown_TmpImpl extends SchumacherFM_Markdown_Model_Michelf_Markdown
16
+ {
17
+
18
+ ### Configuration Variables ###
19
+
20
+ # Prefix for footnote ids.
21
+ public $fn_id_prefix = "";
22
+
23
+ # Optional title attribute for footnote links and backlinks.
24
+ public $fn_link_title = "";
25
+ public $fn_backlink_title = "";
26
+
27
+ # Optional class attribute for footnote links and backlinks.
28
+ public $fn_link_class = "footnote-ref";
29
+ public $fn_backlink_class = "footnote-backref";
30
+
31
+ # Class name for table cell alignment (%% replaced left/center/right)
32
+ # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
33
+ # If empty, the align attribute is used instead of a class name.
34
+ public $table_align_class_tmpl = '';
35
+
36
+ # Optional class prefix for fenced code block.
37
+ public $code_class_prefix = "";
38
+ # Class attribute for code blocks goes on the `code` tag;
39
+ # setting this to true will put attributes on the `pre` tag instead.
40
+ public $code_attr_on_pre = FALSE;
41
+
42
+ # Predefined abbreviations.
43
+ public $predef_abbr = array();
44
+
45
+ ### Parser Implementation ###
46
+
47
+ public function __construct()
48
+ {
49
+ #
50
+ # Constructor function. Initialize the parser object.
51
+ #
52
+ # Add extra escapable characters before parent constructor
53
+ # initialize the table.
54
+ $this->escape_chars .= ':|';
55
+
56
+ # Insert extra document, block, and span transformations.
57
+ # Parent constructor will do the sorting.
58
+ $this->document_gamut += array(
59
+ "doFencedCodeBlocks" => 5,
60
+ "stripFootnotes" => 15,
61
+ "stripAbbreviations" => 25,
62
+ "appendFootnotes" => 50,
63
+ );
64
+ $this->block_gamut += array(
65
+ "doFencedCodeBlocks" => 5,
66
+ "doTables" => 15,
67
+ "doDefLists" => 45,
68
+ );
69
+ $this->span_gamut += array(
70
+ "doFootnotes" => 5,
71
+ "doAbbreviations" => 70,
72
+ );
73
+
74
+ parent::__construct();
75
+ }
76
+
77
+ # Extra variables used during extra transformations.
78
+ protected $footnotes = array();
79
+ protected $footnotes_ordered = array();
80
+ protected $footnotes_ref_count = array();
81
+ protected $footnotes_numbers = array();
82
+ protected $abbr_desciptions = array();
83
+ protected $abbr_word_re = '';
84
+
85
+ # Give the current footnote number.
86
+ protected $footnote_counter = 1;
87
+
88
+ protected function setup()
89
+ {
90
+ #
91
+ # Setting up Extra-specific variables.
92
+ #
93
+ parent::setup();
94
+
95
+ $this->footnotes = array();
96
+ $this->footnotes_ordered = array();
97
+ $this->footnotes_ref_count = array();
98
+ $this->footnotes_numbers = array();
99
+ $this->abbr_desciptions = array();
100
+ $this->abbr_word_re = '';
101
+ $this->footnote_counter = 1;
102
+
103
+ foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
104
+ if ($this->abbr_word_re)
105
+ $this->abbr_word_re .= '|';
106
+ $this->abbr_word_re .= preg_quote($abbr_word);
107
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
108
+ }
109
+ }
110
+
111
+ protected function teardown()
112
+ {
113
+ #
114
+ # Clearing Extra-specific variables.
115
+ #
116
+ $this->footnotes = array();
117
+ $this->footnotes_ordered = array();
118
+ $this->footnotes_ref_count = array();
119
+ $this->footnotes_numbers = array();
120
+ $this->abbr_desciptions = array();
121
+ $this->abbr_word_re = '';
122
+
123
+ parent::teardown();
124
+ }
125
+
126
+ ### Extra Attribute Parser ###
127
+
128
+ # Expression to use to catch attributes (includes the braces)
129
+ protected $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}';
130
+ # Expression to use when parsing in a context when no capture is desired
131
+ protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}';
132
+
133
+ protected function doExtraAttributes($tag_name, $attr)
134
+ {
135
+ #
136
+ # Parse attributes caught by the $this->id_class_attr_catch_re expression
137
+ # and return the HTML-formatted list of attributes.
138
+ #
139
+ # Currently supported attributes are .class and #id.
140
+ #
141
+ if (empty($attr)) return "";
142
+
143
+ # Split on components
144
+ preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches);
145
+ $elements = $matches[0];
146
+
147
+ # handle classes and ids (only first id taken into account)
148
+ $classes = array();
149
+ $id = FALSE;
150
+ foreach ($elements as $element) {
151
+ if ($element{0} == '.') {
152
+ $classes[] = substr($element, 1);
153
+ } else if ($element{0} == '#') {
154
+ if ($id === FALSE) $id = substr($element, 1);
155
+ }
156
+ }
157
+
158
+ # compose attributes as string
159
+ $attr_str = "";
160
+ if (!empty($id)) {
161
+ $attr_str .= ' id="' . $id . '"';
162
+ }
163
+ if (!empty($classes)) {
164
+ $attr_str .= ' class="' . implode(" ", $classes) . '"';
165
+ }
166
+ return $attr_str;
167
+ }
168
+
169
+ protected function stripLinkDefinitions($text)
170
+ {
171
+ #
172
+ # Strips link definitions from text, stores the URLs and titles in
173
+ # hash references.
174
+ #
175
+ $less_than_tab = $this->tab_width - 1;
176
+
177
+ # Link defs are in the form: ^[id]: url "optional title"
178
+ $text = preg_replace_callback('{
179
+ ^[ ]{0,' . $less_than_tab . '}\[(.+)\][ ]?: # id = $1
180
+ [ ]*
181
+ \n? # maybe *one* newline
182
+ [ ]*
183
+ (?:
184
+ <(.+?)> # url = $2
185
+ |
186
+ (\S+?) # url = $3
187
+ )
188
+ [ ]*
189
+ \n? # maybe one newline
190
+ [ ]*
191
+ (?:
192
+ (?<=\s) # lookbehind for whitespace
193
+ ["(]
194
+ (.*?) # title = $4
195
+ [")]
196
+ [ ]*
197
+ )? # title is optional
198
+ (?:[ ]* ' . $this->id_class_attr_catch_re . ' )? # $5 = extra id & class attr
199
+ (?:\n+|\Z)
200
+ }xm',
201
+ array(&$this, '_stripLinkDefinitions_callback'),
202
+ $text);
203
+ return $text;
204
+ }
205
+
206
+ protected function _stripLinkDefinitions_callback($matches)
207
+ {
208
+ $link_id = strtolower($matches[1]);
209
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
210
+ $this->urls[$link_id] = $url;
211
+ $this->titles[$link_id] =& $matches[4];
212
+ $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
213
+ return ''; # String that will replace the block
214
+ }
215
+
216
+ ### HTML Block Parser ###
217
+
218
+ # Tags that are always treated as block tags:
219
+ protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption';
220
+
221
+ # Tags treated as block tags only if the opening tag is alone on its line:
222
+ protected $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
223
+
224
+ # Tags where markdown="1" default to span mode:
225
+ protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
226
+
227
+ # Tags which must not have their contents modified, no matter where
228
+ # they appear:
229
+ protected $clean_tags_re = 'script|math|svg';
230
+
231
+ # Tags that do not need to be closed.
232
+ protected $auto_close_tags_re = 'hr|img|param|source|track';
233
+
234
+ protected function hashHTMLBlocks($text)
235
+ {
236
+ #
237
+ # Hashify HTML Blocks and "clean tags".
238
+ #
239
+ # We only want to do this for block-level HTML tags, such as headers,
240
+ # lists, and tables. That's because we still want to wrap <p>s around
241
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
242
+ # phrase emphasis, and spans. The list of tags we're looking for is
243
+ # hard-coded.
244
+ #
245
+ # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
246
+ # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
247
+ # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
248
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
249
+ # These two functions are calling each other. It's recursive!
250
+ #
251
+ if ($this->no_markup) return $text;
252
+
253
+ #
254
+ # Call the HTML-in-Markdown hasher.
255
+ #
256
+ list($text,) = $this->_hashHTMLBlocks_inMarkdown($text);
257
+
258
+ return $text;
259
+ }
260
+
261
+ protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
262
+ $enclosing_tag_re = '', $span = FALSE)
263
+ {
264
+ #
265
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
266
+ #
267
+ # * $indent is the number of space to be ignored when checking for code
268
+ # blocks. This is important because if we don't take the indent into
269
+ # account, something like this (which looks right) won't work as expected:
270
+ #
271
+ # <div>
272
+ # <div markdown="1">
273
+ # Hello World. <-- Is this a Markdown code block or text?
274
+ # </div> <-- Is this a Markdown code block or a real tag?
275
+ # <div>
276
+ #
277
+ # If you don't like this, just don't indent the tag on which
278
+ # you apply the markdown="1" attribute.
279
+ #
280
+ # * If $enclosing_tag_re is not empty, stops at the first unmatched closing
281
+ # tag with that name. Nested tags supported.
282
+ #
283
+ # * If $span is true, text inside must treated as span. So any double
284
+ # newline will be replaced by a single newline so that it does not create
285
+ # paragraphs.
286
+ #
287
+ # Returns an array of that form: ( processed text , remaining text )
288
+ #
289
+ if ($text === '') return array('', '');
290
+
291
+ # Regex to check for the presense of newlines around a block tag.
292
+ $newline_before_re = '/(?:^\n?|\n\n)*$/';
293
+ $newline_after_re =
294
+ '{
295
+ ^ # Start of text following the tag.
296
+ (?>[ ]*<!--.*?-->)? # Optional comment.
297
+ [ ]*\n # Must be followed by newline.
298
+ }xs';
299
+
300
+ # Regex to match any tag.
301
+ $block_tag_re =
302
+ '{
303
+ ( # $2: Capture whole tag.
304
+ </? # Any opening or closing tag.
305
+ (?> # Tag name.
306
+ ' . $this->block_tags_re . ' |
307
+ ' . $this->context_block_tags_re . ' |
308
+ ' . $this->clean_tags_re . ' |
309
+ (?!\s)' . $enclosing_tag_re . '
310
+ )
311
+ (?:
312
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
313
+ (?>
314
+ ".*?" | # Double quotes (can contain `>`)
315
+ \'.*?\' | # Single quotes (can contain `>`)
316
+ .+? # Anything but quotes and `>`.
317
+ )*?
318
+ )?
319
+ > # End of tag.
320
+ |
321
+ <!-- .*? --> # HTML Comment
322
+ |
323
+ <\?.*?\?> | <%.*?%> # Processing instruction
324
+ |
325
+ <!\[CDATA\[.*?\]\]> # CData Block
326
+ |
327
+ # Code span marker
328
+ `+
329
+ ' . (!$span ? ' # If not in span.
330
+ |
331
+ # Indented code block
332
+ (?: ^[ ]*\n | ^ | \n[ ]*\n )
333
+ [ ]{' . ($indent + 4) . '}[^\n]* \n
334
+ (?>
335
+ (?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n
336
+ )*
337
+ |
338
+ # Fenced code block marker
339
+ (?<= ^ | \n )
340
+ [ ]{0,' . ($indent + 3) . '}~{3,}
341
+ [ ]*
342
+ (?:
343
+ \.?[-_:a-zA-Z0-9]+ # standalone class name
344
+ |
345
+ ' . $this->id_class_attr_nocatch_re . ' # extra attributes
346
+ )?
347
+ [ ]*
348
+ \n
349
+ ' : '') . ' # End (if not is span).
350
+ )
351
+ }xs';
352
+
353
+ $depth = 0; # Current depth inside the tag tree.
354
+ $parsed = ""; # Parsed text that will be returned.
355
+
356
+ #
357
+ # Loop through every tag until we find the closing tag of the parent
358
+ # or loop until reaching the end of text if no parent tag specified.
359
+ #
360
+ do {
361
+ #
362
+ # Split the text using the first $tag_match pattern found.
363
+ # Text before pattern will be first in the array, text after
364
+ # pattern will be at the end, and between will be any catches made
365
+ # by the pattern.
366
+ #
367
+ $parts = preg_split($block_tag_re, $text, 2,
368
+ PREG_SPLIT_DELIM_CAPTURE);
369
+
370
+ # If in Markdown span mode, add a empty-string span-level hash
371
+ # after each newline to prevent triggering any block element.
372
+ if ($span) {
373
+ $void = $this->hashPart("", ':');
374
+ $newline = "$void\n";
375
+ $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
376
+ }
377
+
378
+ $parsed .= $parts[0]; # Text before current tag.
379
+
380
+ # If end of $text has been reached. Stop loop.
381
+ if (count($parts) < 3) {
382
+ $text = "";
383
+ break;
384
+ }
385
+
386
+ $tag = $parts[1]; # Tag to handle.
387
+ $text = $parts[2]; # Remaining text after current tag.
388
+ $tag_re = preg_quote($tag); # For use in a regular expression.
389
+
390
+ #
391
+ # Check for: Code span marker
392
+ #
393
+ if ($tag{0} == "`") {
394
+ # Find corresponding end marker.
395
+ $tag_re = preg_quote($tag);
396
+ if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}',
397
+ $text, $matches)
398
+ ) {
399
+ # End marker found: pass text unchanged until marker.
400
+ $parsed .= $tag . $matches[0];
401
+ $text = substr($text, strlen($matches[0]));
402
+ } else {
403
+ # Unmatched marker: just skip it.
404
+ $parsed .= $tag;
405
+ }
406
+ } #
407
+ # Check for: Fenced code block marker.
408
+ #
409
+ else if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~+)}', $tag, $capture)) {
410
+ # Fenced code block marker: find matching end marker.
411
+ $fence_indent = strlen($capture[1]); # use captured indent in re
412
+ $fence_re = $capture[2]; # use captured fence in re
413
+ if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text,
414
+ $matches)
415
+ ) {
416
+ # End marker found: pass text unchanged until marker.
417
+ $parsed .= $tag . $matches[0];
418
+ $text = substr($text, strlen($matches[0]));
419
+ } else {
420
+ # No end marker: just skip it.
421
+ $parsed .= $tag;
422
+ }
423
+ } #
424
+ # Check for: Indented code block.
425
+ #
426
+ else if ($tag{0} == "\n" || $tag{0} == " ") {
427
+ # Indented code block: pass it unchanged, will be handled
428
+ # later.
429
+ $parsed .= $tag;
430
+ } #
431
+ # Check for: Opening Block level tag or
432
+ # Opening Context Block tag (like ins and del)
433
+ # used as a block tag (tag is alone on it's line).
434
+ #
435
+ else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
436
+ (preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
437
+ preg_match($newline_before_re, $parsed) &&
438
+ preg_match($newline_after_re, $text))
439
+ ) {
440
+ # Need to parse tag and following text using the HTML parser.
441
+ list($block_text, $text) =
442
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", TRUE);
443
+
444
+ # Make sure it stays outside of any paragraph by adding newlines.
445
+ $parsed .= "\n\n$block_text\n\n";
446
+ } #
447
+ # Check for: Clean tag (like script, math)
448
+ # HTML Comments, processing instructions.
449
+ #
450
+ else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
451
+ $tag{1} == '!' || $tag{1} == '?'
452
+ ) {
453
+ # Need to parse tag and following text using the HTML parser.
454
+ # (don't check for markdown attribute)
455
+ list($block_text, $text) =
456
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", FALSE);
457
+
458
+ $parsed .= $block_text;
459
+ } #
460
+ # Check for: Tag with same name as enclosing tag.
461
+ #
462
+ else if ($enclosing_tag_re !== '' &&
463
+ # Same name as enclosing tag.
464
+ preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag)
465
+ ) {
466
+ #
467
+ # Increase/decrease nested tag count.
468
+ #
469
+ if ($tag{1} == '/') $depth--;
470
+ else if ($tag{strlen($tag) - 2} != '/') $depth++;
471
+
472
+ if ($depth < 0) {
473
+ #
474
+ # Going out of parent element. Clean up and break so we
475
+ # return to the calling function.
476
+ #
477
+ $text = $tag . $text;
478
+ break;
479
+ }
480
+
481
+ $parsed .= $tag;
482
+ } else {
483
+ $parsed .= $tag;
484
+ }
485
+ } while ($depth >= 0);
486
+
487
+ return array($parsed, $text);
488
+ }
489
+
490
+ protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr)
491
+ {
492
+ #
493
+ # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
494
+ #
495
+ # * Calls $hash_method to convert any blocks.
496
+ # * Stops when the first opening tag closes.
497
+ # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
498
+ # (it is not inside clean tags)
499
+ #
500
+ # Returns an array of that form: ( processed text , remaining text )
501
+ #
502
+ if ($text === '') return array('', '');
503
+
504
+ # Regex to match `markdown` attribute inside of a tag.
505
+ $markdown_attr_re = '
506
+ {
507
+ \s* # Eat whitespace before the `markdown` attribute
508
+ markdown
509
+ \s*=\s*
510
+ (?>
511
+ (["\']) # $1: quote delimiter
512
+ (.*?) # $2: attribute value
513
+ \1 # matching delimiter
514
+ |
515
+ ([^\s>]*) # $3: unquoted attribute value
516
+ )
517
+ () # $4: make $3 always defined (avoid warnings)
518
+ }xs';
519
+
520
+ # Regex to match any tag.
521
+ $tag_re = '{
522
+ ( # $2: Capture whole tag.
523
+ </? # Any opening or closing tag.
524
+ [\w:$]+ # Tag name.
525
+ (?:
526
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
527
+ (?>
528
+ ".*?" | # Double quotes (can contain `>`)
529
+ \'.*?\' | # Single quotes (can contain `>`)
530
+ .+? # Anything but quotes and `>`.
531
+ )*?
532
+ )?
533
+ > # End of tag.
534
+ |
535
+ <!-- .*? --> # HTML Comment
536
+ |
537
+ <\?.*?\?> | <%.*?%> # Processing instruction
538
+ |
539
+ <!\[CDATA\[.*?\]\]> # CData Block
540
+ )
541
+ }xs';
542
+
543
+ $original_text = $text; # Save original text in case of faliure.
544
+
545
+ $depth = 0; # Current depth inside the tag tree.
546
+ $block_text = ""; # Temporary text holder for current text.
547
+ $parsed = ""; # Parsed text that will be returned.
548
+
549
+ #
550
+ # Get the name of the starting tag.
551
+ # (This pattern makes $base_tag_name_re safe without quoting.)
552
+ #
553
+ if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
554
+ $base_tag_name_re = $matches[1];
555
+
556
+ #
557
+ # Loop through every tag until we find the corresponding closing tag.
558
+ #
559
+ do {
560
+ #
561
+ # Split the text using the first $tag_match pattern found.
562
+ # Text before pattern will be first in the array, text after
563
+ # pattern will be at the end, and between will be any catches made
564
+ # by the pattern.
565
+ #
566
+ $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
567
+
568
+ if (count($parts) < 3) {
569
+ #
570
+ # End of $text reached with unbalenced tag(s).
571
+ # In that case, we return original text unchanged and pass the
572
+ # first character as filtered to prevent an infinite loop in the
573
+ # parent function.
574
+ #
575
+ return array($original_text{0}, substr($original_text, 1));
576
+ }
577
+
578
+ $block_text .= $parts[0]; # Text before current tag.
579
+ $tag = $parts[1]; # Tag to handle.
580
+ $text = $parts[2]; # Remaining text after current tag.
581
+
582
+ #
583
+ # Check for: Auto-close tag (like <hr/>)
584
+ # Comments and Processing Instructions.
585
+ #
586
+ if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) ||
587
+ $tag{1} == '!' || $tag{1} == '?'
588
+ ) {
589
+ # Just add the tag to the block as if it was text.
590
+ $block_text .= $tag;
591
+ } else {
592
+ #
593
+ # Increase/decrease nested tag count. Only do so if
594
+ # the tag's name match base tag's.
595
+ #
596
+ if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) {
597
+ if ($tag{1} == '/') $depth--;
598
+ else if ($tag{strlen($tag) - 2} != '/') $depth++;
599
+ }
600
+
601
+ #
602
+ # Check for `markdown="1"` attribute and handle it.
603
+ #
604
+ if ($md_attr &&
605
+ preg_match($markdown_attr_re, $tag, $attr_m) &&
606
+ preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])
607
+ ) {
608
+ # Remove `markdown` attribute from opening tag.
609
+ $tag = preg_replace($markdown_attr_re, '', $tag);
610
+
611
+ # Check if text inside this tag must be parsed in span mode.
612
+ $this->mode = $attr_m[2] . $attr_m[3];
613
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
614
+ preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag);
615
+
616
+ # Calculate indent before tag.
617
+ if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
618
+ $strlen = $this->utf8_strlen;
619
+ $indent = $strlen($matches[1], 'UTF-8');
620
+ } else {
621
+ $indent = 0;
622
+ }
623
+
624
+ # End preceding block with this tag.
625
+ $block_text .= $tag;
626
+ $parsed .= $this->$hash_method($block_text);
627
+
628
+ # Get enclosing tag name for the ParseMarkdown function.
629
+ # (This pattern makes $tag_name_re safe without quoting.)
630
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
631
+ $tag_name_re = $matches[1];
632
+
633
+ # Parse the content using the HTML-in-Markdown parser.
634
+ list ($block_text, $text)
635
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
636
+ $tag_name_re, $span_mode);
637
+
638
+ # Outdent markdown text.
639
+ if ($indent > 0) {
640
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
641
+ $block_text);
642
+ }
643
+
644
+ # Append tag content to parsed text.
645
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
646
+ else $parsed .= "$block_text";
647
+
648
+ # Start over with a new block.
649
+ $block_text = "";
650
+ } else $block_text .= $tag;
651
+ }
652
+
653
+ } while ($depth > 0);
654
+
655
+ #
656
+ # Hash last block text that wasn't processed inside the loop.
657
+ #
658
+ $parsed .= $this->$hash_method($block_text);
659
+
660
+ return array($parsed, $text);
661
+ }
662
+
663
+ protected function hashClean($text)
664
+ {
665
+ #
666
+ # Called whenever a tag must be hashed when a function inserts a "clean" tag
667
+ # in $text, it passes through this function and is automaticaly escaped,
668
+ # blocking invalid nested overlap.
669
+ #
670
+ return $this->hashPart($text, 'C');
671
+ }
672
+
673
+ protected function doAnchors($text)
674
+ {
675
+ #
676
+ # Turn Markdown link shortcuts into XHTML <a> tags.
677
+ #
678
+ if ($this->in_anchor) return $text;
679
+ $this->in_anchor = TRUE;
680
+
681
+ #
682
+ # First, handle reference-style links: [link text] [id]
683
+ #
684
+ $text = preg_replace_callback('{
685
+ ( # wrap whole match in $1
686
+ \[
687
+ (' . $this->nested_brackets_re . ') # link text = $2
688
+ \]
689
+
690
+ [ ]? # one optional space
691
+ (?:\n[ ]*)? # one optional newline followed by spaces
692
+
693
+ \[
694
+ (.*?) # id = $3
695
+ \]
696
+ )
697
+ }xs',
698
+ array(&$this, '_doAnchors_reference_callback'), $text);
699
+
700
+ #
701
+ # Next, inline-style links: [link text](url "optional title")
702
+ #
703
+ $text = preg_replace_callback('{
704
+ ( # wrap whole match in $1
705
+ \[
706
+ (' . $this->nested_brackets_re . ') # link text = $2
707
+ \]
708
+ \( # literal paren
709
+ [ \n]*
710
+ (?:
711
+ <(.+?)> # href = $3
712
+ |
713
+ (' . $this->nested_url_parenthesis_re . ') # href = $4
714
+ )
715
+ [ \n]*
716
+ ( # $5
717
+ ([\'"]) # quote char = $6
718
+ (.*?) # Title = $7
719
+ \6 # matching quote
720
+ [ \n]* # ignore any spaces/tabs between closing quote and )
721
+ )? # title is optional
722
+ \)
723
+ (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes
724
+ )
725
+ }xs',
726
+ array(&$this, '_doAnchors_inline_callback'), $text);
727
+
728
+ #
729
+ # Last, handle reference-style shortcuts: [link text]
730
+ # These must come last in case you've also got [link text][1]
731
+ # or [link text](/foo)
732
+ #
733
+ $text = preg_replace_callback('{
734
+ ( # wrap whole match in $1
735
+ \[
736
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
737
+ \]
738
+ )
739
+ }xs',
740
+ array(&$this, '_doAnchors_reference_callback'), $text);
741
+
742
+ $this->in_anchor = FALSE;
743
+ return $text;
744
+ }
745
+
746
+ protected function _doAnchors_reference_callback($matches)
747
+ {
748
+ $whole_match = $matches[1];
749
+ $link_text = $matches[2];
750
+ $link_id =& $matches[3];
751
+
752
+ if ($link_id == "") {
753
+ # for shortcut links like [this][] or [this].
754
+ $link_id = $link_text;
755
+ }
756
+
757
+ # lower-case and turn embedded newlines into spaces
758
+ $link_id = strtolower($link_id);
759
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
760
+
761
+ if (isset($this->urls[$link_id])) {
762
+ $url = $this->urls[$link_id];
763
+ $url = $this->encodeAttribute($url);
764
+
765
+ $result = "<a href=\"$url\"";
766
+ if (isset($this->titles[$link_id])) {
767
+ $title = $this->titles[$link_id];
768
+ $title = $this->encodeAttribute($title);
769
+ $result .= " title=\"$title\"";
770
+ }
771
+ if (isset($this->ref_attr[$link_id]))
772
+ $result .= $this->ref_attr[$link_id];
773
+
774
+ $link_text = $this->runSpanGamut($link_text);
775
+ $result .= ">$link_text</a>";
776
+ $result = $this->hashPart($result);
777
+ } else {
778
+ $result = $whole_match;
779
+ }
780
+ return $result;
781
+ }
782
+
783
+ protected function _doAnchors_inline_callback($matches)
784
+ {
785
+ $whole_match = $matches[1];
786
+ $link_text = $this->runSpanGamut($matches[2]);
787
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
788
+ $title =& $matches[7];
789
+ $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
790
+
791
+ $url = $this->encodeAttribute($url);
792
+
793
+ $result = "<a href=\"$url\"";
794
+ if (isset($title)) {
795
+ $title = $this->encodeAttribute($title);
796
+ $result .= " title=\"$title\"";
797
+ }
798
+ $result .= $attr;
799
+
800
+ $link_text = $this->runSpanGamut($link_text);
801
+ $result .= ">$link_text</a>";
802
+
803
+ return $this->hashPart($result);
804
+ }
805
+
806
+ protected function doImages($text)
807
+ {
808
+ #
809
+ # Turn Markdown image shortcuts into <img> tags.
810
+ #
811
+ #
812
+ # First, handle reference-style labeled images: ![alt text][id]
813
+ #
814
+ $text = preg_replace_callback('{
815
+ ( # wrap whole match in $1
816
+ !\[
817
+ (' . $this->nested_brackets_re . ') # alt text = $2
818
+ \]
819
+
820
+ [ ]? # one optional space
821
+ (?:\n[ ]*)? # one optional newline followed by spaces
822
+
823
+ \[
824
+ (.*?) # id = $3
825
+ \]
826
+
827
+ )
828
+ }xs',
829
+ array(&$this, '_doImages_reference_callback'), $text);
830
+
831
+ #
832
+ # Next, handle inline images: ![alt text](url "optional title")
833
+ # Don't forget: encode * and _
834
+ #
835
+ $text = preg_replace_callback('{
836
+ ( # wrap whole match in $1
837
+ !\[
838
+ (' . $this->nested_brackets_re . ') # alt text = $2
839
+ \]
840
+ \s? # One optional whitespace character
841
+ \( # literal paren
842
+ [ \n]*
843
+ (?:
844
+ <(\S*)> # src url = $3
845
+ |
846
+ (' . $this->nested_url_parenthesis_re . ') # src url = $4
847
+ )
848
+ [ \n]*
849
+ ( # $5
850
+ ([\'"]) # quote char = $6
851
+ (.*?) # title = $7
852
+ \6 # matching quote
853
+ [ \n]*
854
+ )? # title is optional
855
+ \)
856
+ (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes
857
+ )
858
+ }xs',
859
+ array(&$this, '_doImages_inline_callback'), $text);
860
+
861
+ return $text;
862
+ }
863
+
864
+ protected function _doImages_reference_callback($matches)
865
+ {
866
+ $whole_match = $matches[1];
867
+ $alt_text = $matches[2];
868
+ $link_id = strtolower($matches[3]);
869
+
870
+ if ($link_id == "") {
871
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
872
+ }
873
+
874
+ $alt_text = $this->encodeAttribute($alt_text);
875
+ if (isset($this->urls[$link_id])) {
876
+ $url = $this->encodeAttribute($this->urls[$link_id]);
877
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
878
+ if (isset($this->titles[$link_id])) {
879
+ $title = $this->titles[$link_id];
880
+ $title = $this->encodeAttribute($title);
881
+ $result .= " title=\"$title\"";
882
+ }
883
+ if (isset($this->ref_attr[$link_id]))
884
+ $result .= $this->ref_attr[$link_id];
885
+ $result .= $this->empty_element_suffix;
886
+ $result = $this->hashPart($result);
887
+ } else {
888
+ # If there's no such link ID, leave intact:
889
+ $result = $whole_match;
890
+ }
891
+
892
+ return $result;
893
+ }
894
+
895
+ protected function _doImages_inline_callback($matches)
896
+ {
897
+ $whole_match = $matches[1];
898
+ $alt_text = $matches[2];
899
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
900
+ $title =& $matches[7];
901
+ $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
902
+
903
+ $alt_text = $this->encodeAttribute($alt_text);
904
+ $url = $this->encodeAttribute($url);
905
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
906
+ if (isset($title)) {
907
+ $title = $this->encodeAttribute($title);
908
+ $result .= " title=\"$title\""; # $title already quoted
909
+ }
910
+ $result .= $attr;
911
+ $result .= $this->empty_element_suffix;
912
+
913
+ return $this->hashPart($result);
914
+ }
915
+
916
+ protected function doHeaders($text)
917
+ {
918
+ #
919
+ # Redefined to add id and class attribute support.
920
+ #
921
+ # Setext-style headers:
922
+ # Header 1 {#header1}
923
+ # ========
924
+ #
925
+ # Header 2 {#header2 .class1 .class2}
926
+ # --------
927
+ #
928
+ $text = preg_replace_callback(
929
+ '{
930
+ (^.+?) # $1: Header text
931
+ (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes
932
+ [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
933
+ }mx',
934
+ array(&$this, '_doHeaders_callback_setext'), $text);
935
+
936
+ # atx-style headers:
937
+ # # Header 1 {#header1}
938
+ # ## Header 2 {#header2}
939
+ # ## Header 2 with closing hashes ## {#header3.class1.class2}
940
+ # ...
941
+ # ###### Header 6 {.class2}
942
+ #
943
+ $text = preg_replace_callback('{
944
+ ^(\#{1,6}) # $1 = string of #\'s
945
+ [ ]*
946
+ (.+?) # $2 = Header text
947
+ [ ]*
948
+ \#* # optional closing #\'s (not counted)
949
+ (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes
950
+ [ ]*
951
+ \n+
952
+ }xm',
953
+ array(&$this, '_doHeaders_callback_atx'), $text);
954
+
955
+ return $text;
956
+ }
957
+
958
+ protected function _doHeaders_callback_setext($matches)
959
+ {
960
+ if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
961
+ return $matches[0];
962
+ $level = $matches[3]{0} == '=' ? 1 : 2;
963
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
964
+ $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
965
+ return "\n" . $this->hashBlock($block) . "\n\n";
966
+ }
967
+
968
+ protected function _doHeaders_callback_atx($matches)
969
+ {
970
+ $level = strlen($matches[1]);
971
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
972
+ $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
973
+ return "\n" . $this->hashBlock($block) . "\n\n";
974
+ }
975
+
976
+ protected function doTables($text)
977
+ {
978
+ #
979
+ # Form HTML tables.
980
+ #
981
+ $less_than_tab = $this->tab_width - 1;
982
+ #
983
+ # Find tables with leading pipe.
984
+ #
985
+ # | Header 1 | Header 2
986
+ # | -------- | --------
987
+ # | Cell 1 | Cell 2
988
+ # | Cell 3 | Cell 4
989
+ #
990
+ $text = preg_replace_callback('
991
+ {
992
+ ^ # Start of a line
993
+ [ ]{0,' . $less_than_tab . '} # Allowed whitespace.
994
+ [|] # Optional leading pipe (present)
995
+ (.+) \n # $1: Header row (at least one pipe)
996
+
997
+ [ ]{0,' . $less_than_tab . '} # Allowed whitespace.
998
+ [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
999
+
1000
+ ( # $3: Cells
1001
+ (?>
1002
+ [ ]* # Allowed whitespace.
1003
+ [|] .* \n # Row content.
1004
+ )*
1005
+ )
1006
+ (?=\n|\Z) # Stop at final double newline.
1007
+ }xm',
1008
+ array(&$this, '_doTable_leadingPipe_callback'), $text);
1009
+
1010
+ #
1011
+ # Find tables without leading pipe.
1012
+ #
1013
+ # Header 1 | Header 2
1014
+ # -------- | --------
1015
+ # Cell 1 | Cell 2
1016
+ # Cell 3 | Cell 4
1017
+ #
1018
+ $text = preg_replace_callback('
1019
+ {
1020
+ ^ # Start of a line
1021
+ [ ]{0,' . $less_than_tab . '} # Allowed whitespace.
1022
+ (\S.*[|].*) \n # $1: Header row (at least one pipe)
1023
+
1024
+ [ ]{0,' . $less_than_tab . '} # Allowed whitespace.
1025
+ ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
1026
+
1027
+ ( # $3: Cells
1028
+ (?>
1029
+ .* [|] .* \n # Row content
1030
+ )*
1031
+ )
1032
+ (?=\n|\Z) # Stop at final double newline.
1033
+ }xm',
1034
+ array(&$this, '_DoTable_callback'), $text);
1035
+
1036
+ return $text;
1037
+ }
1038
+
1039
+ protected function _doTable_leadingPipe_callback($matches)
1040
+ {
1041
+ $head = $matches[1];
1042
+ $underline = $matches[2];
1043
+ $content = $matches[3];
1044
+
1045
+ # Remove leading pipe for each row.
1046
+ $content = preg_replace('/^ *[|]/m', '', $content);
1047
+
1048
+ return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1049
+ }
1050
+
1051
+ protected function _doTable_makeAlignAttr($alignname)
1052
+ {
1053
+ if (empty($this->table_align_class_tmpl))
1054
+ return " align=\"$alignname\"";
1055
+
1056
+ $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1057
+ return " class=\"$classname\"";
1058
+ }
1059
+
1060
+ protected function _doTable_callback($matches)
1061
+ {
1062
+ $head = $matches[1];
1063
+ $underline = $matches[2];
1064
+ $content = $matches[3];
1065
+
1066
+ # Remove any tailing pipes for each line.
1067
+ $head = preg_replace('/[|] *$/m', '', $head);
1068
+ $underline = preg_replace('/[|] *$/m', '', $underline);
1069
+ $content = preg_replace('/[|] *$/m', '', $content);
1070
+
1071
+ # Reading alignement from header underline.
1072
+ $separators = preg_split('/ *[|] */', $underline);
1073
+ foreach ($separators as $n => $s) {
1074
+ if (preg_match('/^ *-+: *$/', $s))
1075
+ $attr[$n] = $this->_doTable_makeAlignAttr('right');
1076
+ else if (preg_match('/^ *:-+: *$/', $s))
1077
+ $attr[$n] = $this->_doTable_makeAlignAttr('center');
1078
+ else if (preg_match('/^ *:-+ *$/', $s))
1079
+ $attr[$n] = $this->_doTable_makeAlignAttr('left');
1080
+ else
1081
+ $attr[$n] = '';
1082
+ }
1083
+
1084
+ # Parsing span elements, including code spans, character escapes,
1085
+ # and inline HTML tags, so that pipes inside those gets ignored.
1086
+ $head = $this->parseSpan($head);
1087
+ $headers = preg_split('/ *[|] */', $head);
1088
+ $col_count = count($headers);
1089
+ $attr = array_pad($attr, $col_count, '');
1090
+
1091
+ # Write column headers.
1092
+ $text = "<table>\n";
1093
+ $text .= "<thead>\n";
1094
+ $text .= "<tr>\n";
1095
+ foreach ($headers as $n => $header)
1096
+ $text .= " <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1097
+ $text .= "</tr>\n";
1098
+ $text .= "</thead>\n";
1099
+
1100
+ # Split content by row.
1101
+ $rows = explode("\n", trim($content, "\n"));
1102
+
1103
+ $text .= "<tbody>\n";
1104
+ foreach ($rows as $row) {
1105
+ # Parsing span elements, including code spans, character escapes,
1106
+ # and inline HTML tags, so that pipes inside those gets ignored.
1107
+ $row = $this->parseSpan($row);
1108
+
1109
+ # Split row by cell.
1110
+ $row_cells = preg_split('/ *[|] */', $row, $col_count);
1111
+ $row_cells = array_pad($row_cells, $col_count, '');
1112
+
1113
+ $text .= "<tr>\n";
1114
+ foreach ($row_cells as $n => $cell)
1115
+ $text .= " <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1116
+ $text .= "</tr>\n";
1117
+ }
1118
+ $text .= "</tbody>\n";
1119
+ $text .= "</table>";
1120
+
1121
+ return $this->hashBlock($text) . "\n";
1122
+ }
1123
+
1124
+ protected function doDefLists($text)
1125
+ {
1126
+ #
1127
+ # Form HTML definition lists.
1128
+ #
1129
+ $less_than_tab = $this->tab_width - 1;
1130
+
1131
+ # Re-usable pattern to match any entire dl list:
1132
+ $whole_list_re = '(?>
1133
+ ( # $1 = whole list
1134
+ ( # $2
1135
+ [ ]{0,' . $less_than_tab . '}
1136
+ ((?>.*\S.*\n)+) # $3 = defined term
1137
+ \n?
1138
+ [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1139
+ )
1140
+ (?s:.+?)
1141
+ ( # $4
1142
+ \z
1143
+ |
1144
+ \n{2,}
1145
+ (?=\S)
1146
+ (?! # Negative lookahead for another term
1147
+ [ ]{0,' . $less_than_tab . '}
1148
+ (?: \S.*\n )+? # defined term
1149
+ \n?
1150
+ [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1151
+ )
1152
+ (?! # Negative lookahead for another definition
1153
+ [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1154
+ )
1155
+ )
1156
+ )
1157
+ )'; // mx
1158
+
1159
+ $text = preg_replace_callback('{
1160
+ (?>\A\n?|(?<=\n\n))
1161
+ ' . $whole_list_re . '
1162
+ }mx',
1163
+ array(&$this, '_doDefLists_callback'), $text);
1164
+
1165
+ return $text;
1166
+ }
1167
+
1168
+ protected function _doDefLists_callback($matches)
1169
+ {
1170
+ # Re-usable patterns to match list item bullets and number markers:
1171
+ $list = $matches[1];
1172
+
1173
+ # Turn double returns into triple returns, so that we can make a
1174
+ # paragraph for the last item in a list, if necessary:
1175
+ $result = trim($this->processDefListItems($list));
1176
+ $result = "<dl>\n" . $result . "\n</dl>";
1177
+ return $this->hashBlock($result) . "\n\n";
1178
+ }
1179
+
1180
+ protected function processDefListItems($list_str)
1181
+ {
1182
+ #
1183
+ # Process the contents of a single definition list, splitting it
1184
+ # into individual term and definition list items.
1185
+ #
1186
+ $less_than_tab = $this->tab_width - 1;
1187
+
1188
+ # trim trailing blank lines:
1189
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1190
+
1191
+ # Process definition terms.
1192
+ $list_str = preg_replace_callback('{
1193
+ (?>\A\n?|\n\n+) # leading line
1194
+ ( # definition terms = $1
1195
+ [ ]{0,' . $less_than_tab . '} # leading whitespace
1196
+ (?!\:[ ]|[ ]) # negative lookahead for a definition
1197
+ # mark (colon) or more whitespace.
1198
+ (?> \S.* \n)+? # actual term (not whitespace).
1199
+ )
1200
+ (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
1201
+ # with a definition mark.
1202
+ }xm',
1203
+ array(&$this, '_processDefListItems_callback_dt'), $list_str);
1204
+
1205
+ # Process actual definitions.
1206
+ $list_str = preg_replace_callback('{
1207
+ \n(\n+)? # leading line = $1
1208
+ ( # marker space = $2
1209
+ [ ]{0,' . $less_than_tab . '} # whitespace before colon
1210
+ \:[ ]+ # definition mark (colon)
1211
+ )
1212
+ ((?s:.+?)) # definition text = $3
1213
+ (?= \n+ # stop at next definition mark,
1214
+ (?: # next term or end of text
1215
+ [ ]{0,' . $less_than_tab . '} \:[ ] |
1216
+ <dt> | \z
1217
+ )
1218
+ )
1219
+ }xm',
1220
+ array(&$this, '_processDefListItems_callback_dd'), $list_str);
1221
+
1222
+ return $list_str;
1223
+ }
1224
+
1225
+ protected function _processDefListItems_callback_dt($matches)
1226
+ {
1227
+ $terms = explode("\n", trim($matches[1]));
1228
+ $text = '';
1229
+ foreach ($terms as $term) {
1230
+ $term = $this->runSpanGamut(trim($term));
1231
+ $text .= "\n<dt>" . $term . "</dt>";
1232
+ }
1233
+ return $text . "\n";
1234
+ }
1235
+
1236
+ protected function _processDefListItems_callback_dd($matches)
1237
+ {
1238
+ $leading_line = $matches[1];
1239
+ $marker_space = $matches[2];
1240
+ $def = $matches[3];
1241
+
1242
+ if ($leading_line || preg_match('/\n{2,}/', $def)) {
1243
+ # Replace marker with the appropriate whitespace indentation
1244
+ $def = str_repeat(' ', strlen($marker_space)) . $def;
1245
+ $def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1246
+ $def = "\n" . $def . "\n";
1247
+ } else {
1248
+ $def = rtrim($def);
1249
+ $def = $this->runSpanGamut($this->outdent($def));
1250
+ }
1251
+
1252
+ return "\n<dd>" . $def . "</dd>\n";
1253
+ }
1254
+
1255
+ protected function doFencedCodeBlocks($text)
1256
+ {
1257
+ #
1258
+ # Adding the fenced code block syntax to regular Markdown:
1259
+ #
1260
+ # ~~~
1261
+ # Code block
1262
+ # ~~~
1263
+ #
1264
+ $less_than_tab = $this->tab_width;
1265
+
1266
+ $text = preg_replace_callback('{
1267
+ (?:\n|\A)
1268
+ # 1: Opening marker
1269
+ (
1270
+ ~{3,} # Marker: three tilde or more.
1271
+ )
1272
+ [ ]*
1273
+ (?:
1274
+ \.?([-_:a-zA-Z0-9]+) # 2: standalone class name
1275
+ |
1276
+ ' . $this->id_class_attr_catch_re . ' # 3: Extra attributes
1277
+ )?
1278
+ [ ]* \n # Whitespace and newline following marker.
1279
+
1280
+ # 4: Content
1281
+ (
1282
+ (?>
1283
+ (?!\1 [ ]* \n) # Not a closing marker.
1284
+ .*\n+
1285
+ )+
1286
+ )
1287
+
1288
+ # Closing marker.
1289
+ \1 [ ]* \n
1290
+ }xm',
1291
+ array(&$this, '_doFencedCodeBlocks_callback'), $text);
1292
+
1293
+ return $text;
1294
+ }
1295
+
1296
+ protected function _doFencedCodeBlocks_callback($matches)
1297
+ {
1298
+ $classname =& $matches[2];
1299
+ $attrs =& $matches[3];
1300
+ $codeblock = $matches[4];
1301
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1302
+ $codeblock = preg_replace_callback('/^\n+/',
1303
+ array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock);
1304
+
1305
+ if ($classname != "") {
1306
+ if ($classname{0} == '.')
1307
+ $classname = substr($classname, 1);
1308
+ $attr_str = ' class="' . $this->code_class_prefix . $classname . '"';
1309
+ } else {
1310
+ $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs);
1311
+ }
1312
+ $pre_attr_str = $this->code_attr_on_pre ? $attr_str : '';
1313
+ $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1314
+ $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1315
+
1316
+ return "\n\n" . $this->hashBlock($codeblock) . "\n\n";
1317
+ }
1318
+
1319
+ protected function _doFencedCodeBlocks_newlines($matches)
1320
+ {
1321
+ return str_repeat("<br$this->empty_element_suffix",
1322
+ strlen($matches[0]));
1323
+ }
1324
+
1325
+ #
1326
+ # Redefining emphasis markers so that emphasis by underscore does not
1327
+ # work in the middle of a word.
1328
+ #
1329
+ protected $em_relist = array(
1330
+ '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?=\S|$)(?![\.,:;]\s)',
1331
+ '*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
1332
+ '_' => '(?<=\S|^)(?<!_)_(?![a-zA-Z0-9_])',
1333
+ );
1334
+ protected $strong_relist = array(
1335
+ '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?=\S|$)(?![\.,:;]\s)',
1336
+ '**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
1337
+ '__' => '(?<=\S|^)(?<!_)__(?![a-zA-Z0-9_])',
1338
+ );
1339
+ protected $em_strong_relist = array(
1340
+ '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?=\S|$)(?![\.,:;]\s)',
1341
+ '***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
1342
+ '___' => '(?<=\S|^)(?<!_)___(?![a-zA-Z0-9_])',
1343
+ );
1344
+
1345
+ protected function formParagraphs($text)
1346
+ {
1347
+ #
1348
+ # Params:
1349
+ # $text - string to process with html <p> tags
1350
+ #
1351
+ # Strip leading and trailing lines:
1352
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
1353
+
1354
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1355
+
1356
+ #
1357
+ # Wrap <p> tags and unhashify HTML blocks
1358
+ #
1359
+ foreach ($grafs as $key => $value) {
1360
+ $value = trim($this->runSpanGamut($value));
1361
+
1362
+ # Check if this should be enclosed in a paragraph.
1363
+ # Clean tag hashes & block tag hashes are left alone.
1364
+ $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1365
+
1366
+ if ($is_p) {
1367
+ $value = "<p>$value</p>";
1368
+ }
1369
+ $grafs[$key] = $value;
1370
+ }
1371
+
1372
+ # Join grafs in one text, then unhash HTML tags.
1373
+ $text = implode("\n\n", $grafs);
1374
+
1375
+ # Finish by removing any tag hashes still present in $text.
1376
+ $text = $this->unhash($text);
1377
+
1378
+ return $text;
1379
+ }
1380
+
1381
+ ### Footnotes
1382
+
1383
+ protected function stripFootnotes($text)
1384
+ {
1385
+ #
1386
+ # Strips link definitions from text, stores the URLs and titles in
1387
+ # hash references.
1388
+ #
1389
+ $less_than_tab = $this->tab_width - 1;
1390
+
1391
+ # Link defs are in the form: [^id]: url "optional title"
1392
+ $text = preg_replace_callback('{
1393
+ ^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?: # note_id = $1
1394
+ [ ]*
1395
+ \n? # maybe *one* newline
1396
+ ( # text = $2 (no blank lines allowed)
1397
+ (?:
1398
+ .+ # actual text
1399
+ |
1400
+ \n # newlines but
1401
+ (?!\[\^.+?\]:\s)# negative lookahead for footnote marker.
1402
+ (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
1403
+ # by non-indented content
1404
+ )*
1405
+ )
1406
+ }xm',
1407
+ array(&$this, '_stripFootnotes_callback'),
1408
+ $text);
1409
+ return $text;
1410
+ }
1411
+
1412
+ protected function _stripFootnotes_callback($matches)
1413
+ {
1414
+ $note_id = $this->fn_id_prefix . $matches[1];
1415
+ $this->footnotes[$note_id] = $this->outdent($matches[2]);
1416
+ return ''; # String that will replace the block
1417
+ }
1418
+
1419
+ protected function doFootnotes($text)
1420
+ {
1421
+ #
1422
+ # Replace footnote references in $text [^id] with a special text-token
1423
+ # which will be replaced by the actual footnote marker in appendFootnotes.
1424
+ #
1425
+ if (!$this->in_anchor) {
1426
+ $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1427
+ }
1428
+ return $text;
1429
+ }
1430
+
1431
+ protected function appendFootnotes($text)
1432
+ {
1433
+ #
1434
+ # Append footnote list to text.
1435
+ #
1436
+ $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1437
+ array(&$this, '_appendFootnotes_callback'), $text);
1438
+
1439
+ if (!empty($this->footnotes_ordered)) {
1440
+ $text .= "\n\n";
1441
+ $text .= "<div class=\"footnotes\">\n";
1442
+ $text .= "<hr" . $this->empty_element_suffix . "\n";
1443
+ $text .= "<ol>\n\n";
1444
+
1445
+ $attr = " rev=\"footnote\"";
1446
+ if ($this->fn_backlink_class != "") {
1447
+ $class = $this->fn_backlink_class;
1448
+ $class = $this->encodeAttribute($class);
1449
+ $attr .= " class=\"$class\"";
1450
+ }
1451
+ if ($this->fn_backlink_title != "") {
1452
+ $title = $this->fn_backlink_title;
1453
+ $title = $this->encodeAttribute($title);
1454
+ $attr .= " title=\"$title\"";
1455
+ }
1456
+ $num = 0;
1457
+
1458
+ while (!empty($this->footnotes_ordered)) {
1459
+ $footnote = reset($this->footnotes_ordered);
1460
+ $note_id = key($this->footnotes_ordered);
1461
+ unset($this->footnotes_ordered[$note_id]);
1462
+ $ref_count = $this->footnotes_ref_count[$note_id];
1463
+ unset($this->footnotes_ref_count[$note_id]);
1464
+ unset($this->footnotes[$note_id]);
1465
+
1466
+ $footnote .= "\n"; # Need to append newline before parsing.
1467
+ $footnote = $this->runBlockGamut("$footnote\n");
1468
+ $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1469
+ array(&$this, '_appendFootnotes_callback'), $footnote);
1470
+
1471
+ $attr = str_replace("%%", ++$num, $attr);
1472
+ $note_id = $this->encodeAttribute($note_id);
1473
+
1474
+ # Prepare backlink, multiple backlinks if multiple references
1475
+ $backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>";
1476
+ for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
1477
+ $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>";
1478
+ }
1479
+ # Add backlink to last paragraph; create new paragraph if needed.
1480
+ if (preg_match('{</p>$}', $footnote)) {
1481
+ $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1482
+ } else {
1483
+ $footnote .= "\n\n<p>$backlink</p>";
1484
+ }
1485
+
1486
+ $text .= "<li id=\"fn:$note_id\">\n";
1487
+ $text .= $footnote . "\n";
1488
+ $text .= "</li>\n\n";
1489
+ }
1490
+
1491
+ $text .= "</ol>\n";
1492
+ $text .= "</div>";
1493
+ }
1494
+ return $text;
1495
+ }
1496
+
1497
+ protected function _appendFootnotes_callback($matches)
1498
+ {
1499
+ $node_id = $this->fn_id_prefix . $matches[1];
1500
+
1501
+ # Create footnote marker only if it has a corresponding footnote *and*
1502
+ # the footnote hasn't been used by another marker.
1503
+ if (isset($this->footnotes[$node_id])) {
1504
+ $num =& $this->footnotes_numbers[$node_id];
1505
+ if (!isset($num)) {
1506
+ # Transfer footnote content to the ordered list and give it its
1507
+ # number
1508
+ $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1509
+ $this->footnotes_ref_count[$node_id] = 1;
1510
+ $num = $this->footnote_counter++;
1511
+ $ref_count_mark = '';
1512
+ } else {
1513
+ $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1514
+ }
1515
+
1516
+ $attr = "";
1517
+ if ($this->fn_link_class != "") {
1518
+ $class = $this->fn_link_class;
1519
+ $class = $this->encodeAttribute($class);
1520
+ $attr .= " class=\"$class\"";
1521
+ }
1522
+ if ($this->fn_link_title != "") {
1523
+ $title = $this->fn_link_title;
1524
+ $title = $this->encodeAttribute($title);
1525
+ $attr .= " title=\"$title\"";
1526
+ }
1527
+
1528
+ $attr = str_replace("%%", $num, $attr);
1529
+ $node_id = $this->encodeAttribute($node_id);
1530
+
1531
+ return
1532
+ "<sup id=\"fnref$ref_count_mark:$node_id\">" .
1533
+ "<a href=\"#fn:$node_id\"$attr>$num</a>" .
1534
+ "</sup>";
1535
+ }
1536
+
1537
+ return "[^" . $matches[1] . "]";
1538
+ }
1539
+
1540
+ ### Abbreviations ###
1541
+
1542
+ protected function stripAbbreviations($text)
1543
+ {
1544
+ #
1545
+ # Strips abbreviations from text, stores titles in hash references.
1546
+ #
1547
+ $less_than_tab = $this->tab_width - 1;
1548
+
1549
+ # Link defs are in the form: [id]*: url "optional title"
1550
+ $text = preg_replace_callback('{
1551
+ ^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?: # abbr_id = $1
1552
+ (.*) # text = $2 (no blank lines allowed)
1553
+ }xm',
1554
+ array(&$this, '_stripAbbreviations_callback'),
1555
+ $text);
1556
+ return $text;
1557
+ }
1558
+
1559
+ protected function _stripAbbreviations_callback($matches)
1560
+ {
1561
+ $abbr_word = $matches[1];
1562
+ $abbr_desc = $matches[2];
1563
+ if ($this->abbr_word_re)
1564
+ $this->abbr_word_re .= '|';
1565
+ $this->abbr_word_re .= preg_quote($abbr_word);
1566
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1567
+ return ''; # String that will replace the block
1568
+ }
1569
+
1570
+ protected function doAbbreviations($text)
1571
+ {
1572
+ #
1573
+ # Find defined abbreviations in text and wrap them in <abbr> elements.
1574
+ #
1575
+ if ($this->abbr_word_re) {
1576
+ // cannot use the /x modifier because abbr_word_re may
1577
+ // contain significant spaces:
1578
+ $text = preg_replace_callback('{' .
1579
+ '(?<![\w\x1A])' .
1580
+ '(?:' . $this->abbr_word_re . ')' .
1581
+ '(?![\w\x1A])' .
1582
+ '}',
1583
+ array(&$this, '_doAbbreviations_callback'), $text);
1584
+ }
1585
+ return $text;
1586
+ }
1587
+
1588
+ protected function _doAbbreviations_callback($matches)
1589
+ {
1590
+ $abbr = $matches[0];
1591
+ if (isset($this->abbr_desciptions[$abbr])) {
1592
+ $desc = $this->abbr_desciptions[$abbr];
1593
+ if (empty($desc)) {
1594
+ return $this->hashPart("<abbr>$abbr</abbr>");
1595
+ } else {
1596
+ $desc = $this->encodeAttribute($desc);
1597
+ return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1598
+ }
1599
+ } else {
1600
+ return $matches[0];
1601
+ }
1602
+ }
1603
+
1604
+ }
app/code/community/SchumacherFM/Markdown/Model/Observer/AdminhtmlBlock.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @category SchumacherFM_Markdown
4
+ * @package Helper
5
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
6
+ * @copyright Copyright (c)
7
+ */
8
+ class SchumacherFM_Markdown_Model_Observer_AdminhtmlBlock
9
+ {
10
+ /**
11
+ * adminhtml_block_html_before
12
+ *
13
+ * @param Varien_Event_Observer $observer
14
+ */
15
+ public function alterTextareaBlockTemplate(Varien_Event_Observer $observer)
16
+ {
17
+ if (Mage::helper('markdown')->isDisabled()) {
18
+ return null;
19
+ }
20
+
21
+ /** @var $block Mage_Adminhtml_Block_Template */
22
+ $block = $observer->getEvent()->getBlock();
23
+
24
+ if ($block instanceof Mage_Adminhtml_Block_Catalog_Form_Renderer_Fieldset_Element) {
25
+ /** @var $block Mage_Adminhtml_Block_Catalog_Form_Renderer_Fieldset_Element */
26
+
27
+ /** @var Mage_Adminhtml_Block_Catalog_Helper_Form_Wysiwyg $element */
28
+ $element = $block->getElement();
29
+ if ($this->_isElementAllowed($element)) {
30
+ $this->_getMarkdownButtons($element);
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @param Varien_Data_Form_Element_Abstract $element
37
+ *
38
+ * @return bool
39
+ */
40
+ protected function _isElementAllowed(Varien_Data_Form_Element_Abstract $element)
41
+ {
42
+ $isTextarea = $element instanceof Mage_Adminhtml_Block_Catalog_Helper_Form_Wysiwyg;
43
+ $isDescription = stristr($element->getName(), 'description') !== FALSE && stristr($element->getName(), 'meta') === FALSE;
44
+ return $isDescription && $isTextarea;
45
+ }
46
+
47
+ /**
48
+ * @param Varien_Data_Form_Element_Abstract $element
49
+ */
50
+ protected function _getMarkdownButtons(Varien_Data_Form_Element_Abstract $element)
51
+ {
52
+ $html = $element->getData('after_element_html');
53
+
54
+ $html .= Mage::getSingleton('core/layout')
55
+ ->createBlock('adminhtml/widget_button', '', array(
56
+ 'label' => Mage::helper('markdown')->__('MD enable'),
57
+ 'type' => 'button',
58
+ 'class' => 'btn-wysiwyg',
59
+ 'onclick' => 'toggleMarkdown(\'' .
60
+ rawurlencode(Mage::helper('markdown')->getDetectionTag())
61
+ . '\',\'' . $element->getHtmlId() . '\');'
62
+ ))->toHtml().' ';
63
+
64
+ $html .= Mage::getSingleton('core/layout')
65
+ ->createBlock('adminhtml/widget_button', '', array(
66
+ 'label' => Mage::helper('catalog')->__('Preview Markdown'),
67
+ 'type' => 'button',
68
+ 'class' => 'btn-wysiwyg',
69
+ 'onclick' => 'renderMarkdown(\'' . $element->getHtmlId() . '\')'
70
+ ))->toHtml();
71
+
72
+ $element->setData('after_element_html', $html);
73
+ }
74
+
75
+ }
app/code/community/SchumacherFM/Markdown/etc/adminhtml.xml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ /**
4
+ * @category SchumacherFM
5
+ * @package SchumacherFM_Markdown
6
+ * @copyright Copyright (c) http://www.schumacher.fm
7
+ * @author Cyrill at Schumacher dot fm @SchumacherFM
8
+ */
9
+ -->
10
+ <config>
11
+ <acl>
12
+ <resources>
13
+ <admin>
14
+ <children>
15
+ <system>
16
+ <children>
17
+ <config>
18
+ <children>
19
+ <schumacherfm translate="title" module="schumacherfm_markdown">
20
+ <title>@SchumacherFM (Config)</title>
21
+ </schumacherfm>
22
+ </children>
23
+ </config>
24
+ </children>
25
+ </system>
26
+ </children>
27
+ </admin>
28
+ </resources>
29
+ </acl>
30
+ </config>
app/code/community/SchumacherFM/Markdown/etc/config.xml ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <!--* @category SchumacherFM_Markdown-->
3
+ <!--* @package etc-->
4
+ <!--* @author Cyrill at Schumacher dot fm / @SchumacherFM-->
5
+ <!--* @copyright Copyright (c)-->
6
+ <config>
7
+ <modules>
8
+ <SchumacherFM_Markdown>
9
+ <version>1.0.0</version>
10
+ </SchumacherFM_Markdown>
11
+ </modules>
12
+
13
+ <global>
14
+ <models>
15
+ <markdown>
16
+ <class>SchumacherFM_Markdown_Model</class>
17
+ </markdown>
18
+ </models>
19
+ <resources>
20
+ <markdown_setup>
21
+ <setup>
22
+ <module>SchumacherFM_Markdown</module>
23
+ </setup>
24
+ </markdown_setup>
25
+ </resources>
26
+ <blocks>
27
+ <markdown>
28
+ <class>SchumacherFM_Markdown_Block</class>
29
+ </markdown>
30
+ </blocks>
31
+ <helpers>
32
+ <markdown>
33
+ <class>SchumacherFM_Markdown_Helper</class>
34
+ </markdown>
35
+ </helpers>
36
+ </global>
37
+ <frontend>
38
+ <events>
39
+ <cms_page_render>
40
+ <observers>
41
+ <markdown_renderer>
42
+ <type>singleton</type>
43
+ <class>markdown/markdown_render</class>
44
+ <method>renderPageObserver</method>
45
+ </markdown_renderer>
46
+ </observers>
47
+ </cms_page_render>
48
+
49
+ <core_block_abstract_to_html_after>
50
+ <observers>
51
+ <markdown_renderer>
52
+ <type>singleton</type>
53
+ <class>markdown/markdown_render</class>
54
+ <method>renderBlockObserver</method>
55
+ </markdown_renderer>
56
+ </observers>
57
+ </core_block_abstract_to_html_after>
58
+ </events>
59
+ </frontend>
60
+
61
+ <adminhtml>
62
+ <layout>
63
+ <updates>
64
+ <markdown>
65
+ <file>markdown.xml</file>
66
+ </markdown>
67
+ </updates>
68
+ </layout>
69
+ <translate>
70
+ <modules>
71
+ <SchumacherFM_Markdown>
72
+ <files>
73
+ <default>SchumacherFM_Markdown.csv</default>
74
+ </files>
75
+ </SchumacherFM_Markdown>
76
+ </modules>
77
+ </translate>
78
+ <events>
79
+ <cms_wysiwyg_config_prepare>
80
+ <observers>
81
+ <markdown_observer>
82
+ <class>markdown/editor_observer</class>
83
+ <method>prepareWysiwygPluginConfig</method>
84
+ </markdown_observer>
85
+ </observers>
86
+ </cms_wysiwyg_config_prepare>
87
+ <adminhtml_block_html_before>
88
+ <observers>
89
+ <markdown_adminhtml_block_html_before>
90
+ <class>markdown/observer_adminhtmlBlock</class>
91
+ <method>alterTextareaBlockTemplate</method>
92
+ </markdown_adminhtml_block_html_before>
93
+ </observers>
94
+ </adminhtml_block_html_before>
95
+ <core_config_data_save_after>
96
+ <observers>
97
+ <markdown_core_config_data>
98
+ <class>markdown/editor_observer</class>
99
+ <method>enableDisableWysiwyg</method>
100
+ </markdown_core_config_data>
101
+ </observers>
102
+ </core_config_data_save_after>
103
+ </events>
104
+ </adminhtml>
105
+ <default>
106
+ <schumacherfm>
107
+ <markdown>
108
+ <enable>1</enable>
109
+ <md_extra>0</md_extra>
110
+ <detection_tag>!#markdown</detection_tag>
111
+ </markdown>
112
+ </schumacherfm>
113
+ </default>
114
+ <phpunit>
115
+ <suite>
116
+ <modules>
117
+ <SchumacherFM_Markdown/>
118
+ </modules>
119
+ </suite>
120
+ </phpunit>
121
+ </config>
app/code/community/SchumacherFM/Markdown/etc/system.xml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <!--
3
+ /**
4
+ * @category SchumacherFM
5
+ * @package SchumacherFM_Markdown
6
+ * @copyright Copyright (c) http://www.schumacher.fm
7
+ * @author Cyrill at Schumacher dot fm @SchumacherFM
8
+ */
9
+ -->
10
+ <config>
11
+ <sections>
12
+ <schumacherfm>
13
+ <label>@SchumacherFM</label>
14
+ <tab>advanced</tab>
15
+ <frontend_type>text</frontend_type>
16
+ <sort_order>930</sort_order>
17
+ <show_in_default>1</show_in_default>
18
+ <show_in_website>1</show_in_website>
19
+ <show_in_store>1</show_in_store>
20
+ <groups>
21
+ <markdown translate="label">
22
+ <label>Markdown</label>
23
+ <frontend_type>text</frontend_type>
24
+ <sort_order>50</sort_order>
25
+ <show_in_default>1</show_in_default>
26
+ <show_in_website>1</show_in_website>
27
+ <show_in_store>1</show_in_store>
28
+ <fields>
29
+ <enable translate="label">
30
+ <label>Enable Markdown</label>
31
+ <frontend_type>select</frontend_type>
32
+ <source_model>adminhtml/system_config_source_yesno</source_model>
33
+ <sort_order>10</sort_order>
34
+ <show_in_default>1</show_in_default>
35
+ <show_in_website>1</show_in_website>
36
+ <show_in_store>1</show_in_store>
37
+ </enable>
38
+ <md_extra translate="label">
39
+ <label>Use Markdown Extra</label>
40
+ <frontend_type>select</frontend_type>
41
+ <source_model>adminhtml/system_config_source_yesno</source_model>
42
+ <sort_order>20</sort_order>
43
+ <show_in_default>1</show_in_default>
44
+ <show_in_website>1</show_in_website>
45
+ <show_in_store>1</show_in_store>
46
+ </md_extra>
47
+ <detection_tag translate="label">
48
+ <label>Markdown Detection Tag</label>
49
+ <frontend_type>text</frontend_type>
50
+ <sort_order>30</sort_order>
51
+ <show_in_default>1</show_in_default>
52
+ <show_in_website>1</show_in_website>
53
+ <show_in_store>1</show_in_store>
54
+ <comment>Every content field which contains markdown must have this tag included that it will be parsed.
55
+ This tag will of course be removed during parsing. If empty, every content is considered as markdown.</comment>
56
+ </detection_tag>
57
+ </fields>
58
+ </markdown>
59
+ </groups>
60
+ </schumacherfm>
61
+ </sections>
62
+ </config>
app/code/community/SchumacherFM/Markdown/sql/markdown_setup/install-1.0.0.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /** @var Mage_Core_Model_Resource_Setup $installer */
3
+ $installer = $this;
4
+ $installer->startSetup();
5
+
6
+ /* @var $configurationModel Mage_Core_Model_Config */
7
+ $configurationModel = Mage::getModel('core/config');
8
+ $configurationModel->saveConfig('cms/wysiwyg/enabled', 'disabled');
9
+ $installer->endSetup();
app/design/adminhtml/default/default/layout/markdown.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <layout>
3
+ <editor>
4
+ <reference name="head">
5
+ <action method="addJs"><script>mage/adminhtml/markdown.js</script></action>
6
+ <action method="addJs"><script>mage/adminhtml/marked.js</script></action>
7
+ <action method="addCss"><name>markdown.css</name></action>
8
+ </reference>
9
+ </editor>
10
+ </layout>
app/etc/modules/SchumacherFM_Markdown.xml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <SchumacherFM_Markdown>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ </SchumacherFM_Markdown>
8
+ </modules>
9
+ </config>
app/locale/de_DE/SchumacherFM_Markdown.csv ADDED
File without changes
app/locale/en_US/SchumacherFM_Markdown.csv ADDED
File without changes
js/mage/adminhtml/markdown.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @category SchumacherFM_Markdown
3
+ * @package JavaScript
4
+ * @author Cyrill at Schumacher dot fm / @SchumacherFM
5
+ * @copyright Copyright (c)
6
+ */
7
+ ;
8
+ (function () {
9
+ var FORM_ID = 'edit_form';
10
+ var dialogWindow;
11
+ var dialogWindowId = 'markdown-preview';
12
+ var TEXT_PREFIX = '<div class="markdown">';
13
+ var TEXT_SUFFIX = '</div>';
14
+
15
+ var htmlId = '';
16
+
17
+ var showPreview = function (responseText) {
18
+
19
+ dialogWindow = Dialog.info(TEXT_PREFIX + responseText + TEXT_SUFFIX, {
20
+ draggable: true,
21
+ resizable: true,
22
+ closable: true,
23
+ className: "magento",
24
+ windowClassName: "popup-window",
25
+ title: 'Markdown Preview',
26
+ width: 800,
27
+ height: 480,
28
+ zIndex: 1000,
29
+ recenterAuto: false,
30
+ hideEffect: Element.hide,
31
+ showEffect: Element.show,
32
+ id: dialogWindowId,
33
+ onClose: closeDialogWindow.bind(this)
34
+ });
35
+ }
36
+
37
+ var closeDialogWindow = function (window) {
38
+ if (!window) {
39
+ window = dialogWindow;
40
+ }
41
+ if (window) {
42
+ window.close();
43
+ }
44
+ }
45
+
46
+ var _renderJs = function () {
47
+ showPreview(marked($(htmlId).value));
48
+ }
49
+
50
+ var renderMarkdown = function (Idhtml) {
51
+ htmlId = Idhtml;
52
+ _renderJs();
53
+ return;
54
+
55
+ }
56
+
57
+ var markdownSyntax = function (url, Idhtml) {
58
+ htmlId = Idhtml;
59
+ window.open(url);
60
+ }
61
+
62
+ var toggleMarkdown = function (detectionTag, Idhtml) {
63
+ detectionTag = unescape(detectionTag);
64
+
65
+ if ($(Idhtml).value.indexOf(detectionTag) === -1) {
66
+ $(Idhtml).value = detectionTag + "\n" + $(Idhtml).value;
67
+ }
68
+ alert('Markdown enabled with tag: "' + detectionTag+'"');
69
+ }
70
+
71
+ this.renderMarkdown = renderMarkdown;
72
+ this.markdownSyntax = markdownSyntax;
73
+ this.toggleMarkdown = toggleMarkdown;
74
+
75
+ }).call(function () {
76
+ return this || (typeof window !== 'undefined' ? window : global);
77
+ }());
js/mage/adminhtml/marked.js ADDED
@@ -0,0 +1,1167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * marked - a markdown parser
3
+ * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
4
+ * https://github.com/chjj/marked
5
+ */
6
+
7
+ ;(function () {
8
+
9
+ /**
10
+ * Block-Level Grammar
11
+ */
12
+
13
+ var block = {
14
+ newline: /^\n+/,
15
+ code: /^( {4}[^\n]+\n*)+/,
16
+ fences: noop,
17
+ hr: /^( *[-*_]){3,} *(?:\n+|$)/,
18
+ heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
19
+ nptable: noop,
20
+ lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
21
+ blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
22
+ list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
23
+ html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
24
+ def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
25
+ table: noop,
26
+ paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
27
+ text: /^[^\n]+/
28
+ };
29
+
30
+ block.bullet = /(?:[*+-]|\d+\.)/;
31
+ block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
32
+ block.item = replace(block.item, 'gm')
33
+ (/bull/g, block.bullet)
34
+ ();
35
+
36
+ block.list = replace(block.list)
37
+ (/bull/g, block.bullet)
38
+ ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
39
+ ();
40
+
41
+ block._tag = '(?!(?:'
42
+ + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
43
+ + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
44
+ + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b';
45
+
46
+ block.html = replace(block.html)
47
+ ('comment', /<!--[\s\S]*?-->/)
48
+ ('closed', /<(tag)[\s\S]+?<\/\1>/)
49
+ ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
50
+ (/tag/g, block._tag)
51
+ ();
52
+
53
+ block.paragraph = replace(block.paragraph)
54
+ ('hr', block.hr)
55
+ ('heading', block.heading)
56
+ ('lheading', block.lheading)
57
+ ('blockquote', block.blockquote)
58
+ ('tag', '<' + block._tag)
59
+ ('def', block.def)
60
+ ();
61
+
62
+ /**
63
+ * Normal Block Grammar
64
+ */
65
+
66
+ block.normal = merge({}, block);
67
+
68
+ /**
69
+ * GFM Block Grammar
70
+ */
71
+
72
+ block.gfm = merge({}, block.normal, {
73
+ fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
74
+ paragraph: /^/
75
+ });
76
+
77
+ block.gfm.paragraph = replace(block.paragraph)
78
+ ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
79
+ ();
80
+
81
+ /**
82
+ * GFM + Tables Block Grammar
83
+ */
84
+
85
+ block.tables = merge({}, block.gfm, {
86
+ nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
87
+ table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
88
+ });
89
+
90
+ /**
91
+ * Block Lexer
92
+ */
93
+
94
+ function Lexer(options) {
95
+ this.tokens = [];
96
+ this.tokens.links = {};
97
+ this.options = options || marked.defaults;
98
+ this.rules = block.normal;
99
+
100
+ if (this.options.gfm) {
101
+ if (this.options.tables) {
102
+ this.rules = block.tables;
103
+ } else {
104
+ this.rules = block.gfm;
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Expose Block Rules
111
+ */
112
+
113
+ Lexer.rules = block;
114
+
115
+ /**
116
+ * Static Lex Method
117
+ */
118
+
119
+ Lexer.lex = function (src, options) {
120
+ var lexer = new Lexer(options);
121
+ return lexer.lex(src);
122
+ };
123
+
124
+ /**
125
+ * Preprocessing
126
+ */
127
+
128
+ Lexer.prototype.lex = function (src) {
129
+ src = src
130
+ .replace(/\r\n|\r/g, '\n')
131
+ .replace(/\t/g, ' ')
132
+ .replace(/\u00a0/g, ' ')
133
+ .replace(/\u2424/g, '\n');
134
+
135
+ return this.token(src, true);
136
+ };
137
+
138
+ /**
139
+ * Lexing
140
+ */
141
+
142
+ Lexer.prototype.token = function (src, top) {
143
+ var src = src.replace(/^ +$/gm, '')
144
+ , next
145
+ , loose
146
+ , cap
147
+ , bull
148
+ , b
149
+ , item
150
+ , space
151
+ , i
152
+ , l;
153
+
154
+ while (src) {
155
+ // newline
156
+ if (cap = this.rules.newline.exec(src)) {
157
+ src = src.substring(cap[0].length);
158
+ if (cap[0].length > 1) {
159
+ this.tokens.push({
160
+ type: 'space'
161
+ });
162
+ }
163
+ }
164
+
165
+ // code
166
+ if (cap = this.rules.code.exec(src)) {
167
+ src = src.substring(cap[0].length);
168
+ cap = cap[0].replace(/^ {4}/gm, '');
169
+ this.tokens.push({
170
+ type: 'code',
171
+ text: !this.options.pedantic
172
+ ? cap.replace(/\n+$/, '')
173
+ : cap
174
+ });
175
+ continue;
176
+ }
177
+
178
+ // fences (gfm)
179
+ if (cap = this.rules.fences.exec(src)) {
180
+ src = src.substring(cap[0].length);
181
+ this.tokens.push({
182
+ type: 'code',
183
+ lang: cap[2],
184
+ text: cap[3]
185
+ });
186
+ continue;
187
+ }
188
+
189
+ // heading
190
+ if (cap = this.rules.heading.exec(src)) {
191
+ src = src.substring(cap[0].length);
192
+ this.tokens.push({
193
+ type: 'heading',
194
+ depth: cap[1].length,
195
+ text: cap[2]
196
+ });
197
+ continue;
198
+ }
199
+
200
+ // table no leading pipe (gfm)
201
+ if (top && (cap = this.rules.nptable.exec(src))) {
202
+ src = src.substring(cap[0].length);
203
+
204
+ item = {
205
+ type: 'table',
206
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
207
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
208
+ cells: cap[3].replace(/\n$/, '').split('\n')
209
+ };
210
+
211
+ for (i = 0; i < item.align.length; i++) {
212
+ if (/^ *-+: *$/.test(item.align[i])) {
213
+ item.align[i] = 'right';
214
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
215
+ item.align[i] = 'center';
216
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
217
+ item.align[i] = 'left';
218
+ } else {
219
+ item.align[i] = null;
220
+ }
221
+ }
222
+
223
+ for (i = 0; i < item.cells.length; i++) {
224
+ item.cells[i] = item.cells[i].split(/ *\| */);
225
+ }
226
+
227
+ this.tokens.push(item);
228
+
229
+ continue;
230
+ }
231
+
232
+ // lheading
233
+ if (cap = this.rules.lheading.exec(src)) {
234
+ src = src.substring(cap[0].length);
235
+ this.tokens.push({
236
+ type: 'heading',
237
+ depth: cap[2] === '=' ? 1 : 2,
238
+ text: cap[1]
239
+ });
240
+ continue;
241
+ }
242
+
243
+ // hr
244
+ if (cap = this.rules.hr.exec(src)) {
245
+ src = src.substring(cap[0].length);
246
+ this.tokens.push({
247
+ type: 'hr'
248
+ });
249
+ continue;
250
+ }
251
+
252
+ // blockquote
253
+ if (cap = this.rules.blockquote.exec(src)) {
254
+ src = src.substring(cap[0].length);
255
+
256
+ this.tokens.push({
257
+ type: 'blockquote_start'
258
+ });
259
+
260
+ cap = cap[0].replace(/^ *> ?/gm, '');
261
+
262
+ // Pass `top` to keep the current
263
+ // "toplevel" state. This is exactly
264
+ // how markdown.pl works.
265
+ this.token(cap, top);
266
+
267
+ this.tokens.push({
268
+ type: 'blockquote_end'
269
+ });
270
+
271
+ continue;
272
+ }
273
+
274
+ // list
275
+ if (cap = this.rules.list.exec(src)) {
276
+ src = src.substring(cap[0].length);
277
+ bull = cap[2];
278
+
279
+ this.tokens.push({
280
+ type: 'list_start',
281
+ ordered: bull.length > 1
282
+ });
283
+
284
+ // Get each top-level item.
285
+ cap = cap[0].match(this.rules.item);
286
+
287
+ next = false;
288
+ l = cap.length;
289
+ i = 0;
290
+
291
+ for (; i < l; i++) {
292
+ item = cap[i];
293
+
294
+ // Remove the list item's bullet
295
+ // so it is seen as the next token.
296
+ space = item.length;
297
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
298
+
299
+ // Outdent whatever the
300
+ // list item contains. Hacky.
301
+ if (~item.indexOf('\n ')) {
302
+ space -= item.length;
303
+ item = !this.options.pedantic
304
+ ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
305
+ : item.replace(/^ {1,4}/gm, '');
306
+ }
307
+
308
+ // Determine whether the next list item belongs here.
309
+ // Backpedal if it does not belong in this list.
310
+ if (this.options.smartLists && i !== l - 1) {
311
+ b = block.bullet.exec(cap[i + 1])[0];
312
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
313
+ src = cap.slice(i + 1).join('\n') + src;
314
+ i = l - 1;
315
+ }
316
+ }
317
+
318
+ // Determine whether item is loose or not.
319
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
320
+ // for discount behavior.
321
+ loose = next || /\n\n(?!\s*$)/.test(item);
322
+ if (i !== l - 1) {
323
+ next = item[item.length - 1] === '\n';
324
+ if (!loose) loose = next;
325
+ }
326
+
327
+ this.tokens.push({
328
+ type: loose
329
+ ? 'loose_item_start'
330
+ : 'list_item_start'
331
+ });
332
+
333
+ // Recurse.
334
+ this.token(item, false);
335
+
336
+ this.tokens.push({
337
+ type: 'list_item_end'
338
+ });
339
+ }
340
+
341
+ this.tokens.push({
342
+ type: 'list_end'
343
+ });
344
+
345
+ continue;
346
+ }
347
+
348
+ // html
349
+ if (cap = this.rules.html.exec(src)) {
350
+ src = src.substring(cap[0].length);
351
+ this.tokens.push({
352
+ type: this.options.sanitize
353
+ ? 'paragraph'
354
+ : 'html',
355
+ pre: cap[1] === 'pre' || cap[1] === 'script',
356
+ text: cap[0]
357
+ });
358
+ continue;
359
+ }
360
+
361
+ // def
362
+ if (top && (cap = this.rules.def.exec(src))) {
363
+ src = src.substring(cap[0].length);
364
+ this.tokens.links[cap[1].toLowerCase()] = {
365
+ href: cap[2],
366
+ title: cap[3]
367
+ };
368
+ continue;
369
+ }
370
+
371
+ // table (gfm)
372
+ if (top && (cap = this.rules.table.exec(src))) {
373
+ src = src.substring(cap[0].length);
374
+
375
+ item = {
376
+ type: 'table',
377
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
378
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
379
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
380
+ };
381
+
382
+ for (i = 0; i < item.align.length; i++) {
383
+ if (/^ *-+: *$/.test(item.align[i])) {
384
+ item.align[i] = 'right';
385
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
386
+ item.align[i] = 'center';
387
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
388
+ item.align[i] = 'left';
389
+ } else {
390
+ item.align[i] = null;
391
+ }
392
+ }
393
+
394
+ for (i = 0; i < item.cells.length; i++) {
395
+ item.cells[i] = item.cells[i]
396
+ .replace(/^ *\| *| *\| *$/g, '')
397
+ .split(/ *\| */);
398
+ }
399
+
400
+ this.tokens.push(item);
401
+
402
+ continue;
403
+ }
404
+
405
+ // top-level paragraph
406
+ if (top && (cap = this.rules.paragraph.exec(src))) {
407
+ src = src.substring(cap[0].length);
408
+ this.tokens.push({
409
+ type: 'paragraph',
410
+ text: cap[1][cap[1].length - 1] === '\n'
411
+ ? cap[1].slice(0, -1)
412
+ : cap[1]
413
+ });
414
+ continue;
415
+ }
416
+
417
+ // text
418
+ if (cap = this.rules.text.exec(src)) {
419
+ // Top-level should never reach here.
420
+ src = src.substring(cap[0].length);
421
+ this.tokens.push({
422
+ type: 'text',
423
+ text: cap[0]
424
+ });
425
+ continue;
426
+ }
427
+
428
+ if (src) {
429
+ throw new
430
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
431
+ }
432
+ }
433
+
434
+ return this.tokens;
435
+ };
436
+
437
+ /**
438
+ * Inline-Level Grammar
439
+ */
440
+
441
+ var inline = {
442
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
443
+ autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
444
+ url: noop,
445
+ tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
446
+ link: /^!?\[(inside)\]\(href\)/,
447
+ reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
448
+ nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
449
+ strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
450
+ em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
451
+ code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
452
+ br: /^ {2,}\n(?!\s*$)/,
453
+ del: noop,
454
+ text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
455
+ };
456
+
457
+ inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
458
+ inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
459
+
460
+ inline.link = replace(inline.link)
461
+ ('inside', inline._inside)
462
+ ('href', inline._href)
463
+ ();
464
+
465
+ inline.reflink = replace(inline.reflink)
466
+ ('inside', inline._inside)
467
+ ();
468
+
469
+ /**
470
+ * Normal Inline Grammar
471
+ */
472
+
473
+ inline.normal = merge({}, inline);
474
+
475
+ /**
476
+ * Pedantic Inline Grammar
477
+ */
478
+
479
+ inline.pedantic = merge({}, inline.normal, {
480
+ strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
481
+ em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
482
+ });
483
+
484
+ /**
485
+ * GFM Inline Grammar
486
+ */
487
+
488
+ inline.gfm = merge({}, inline.normal, {
489
+ escape: replace(inline.escape)('])', '~|])')(),
490
+ url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
491
+ del: /^~~(?=\S)([\s\S]*?\S)~~/,
492
+ text: replace(inline.text)
493
+ (']|', '~]|')
494
+ ('|', '|https?://|')
495
+ ()
496
+ });
497
+
498
+ /**
499
+ * GFM + Line Breaks Inline Grammar
500
+ */
501
+
502
+ inline.breaks = merge({}, inline.gfm, {
503
+ br: replace(inline.br)('{2,}', '*')(),
504
+ text: replace(inline.gfm.text)('{2,}', '*')()
505
+ });
506
+
507
+ /**
508
+ * Inline Lexer & Compiler
509
+ */
510
+
511
+ function InlineLexer(links, options) {
512
+ this.options = options || marked.defaults;
513
+ this.links = links;
514
+ this.rules = inline.normal;
515
+
516
+ if (!this.links) {
517
+ throw new
518
+ Error('Tokens array requires a `links` property.');
519
+ }
520
+
521
+ if (this.options.gfm) {
522
+ if (this.options.breaks) {
523
+ this.rules = inline.breaks;
524
+ } else {
525
+ this.rules = inline.gfm;
526
+ }
527
+ } else if (this.options.pedantic) {
528
+ this.rules = inline.pedantic;
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Expose Inline Rules
534
+ */
535
+
536
+ InlineLexer.rules = inline;
537
+
538
+ /**
539
+ * Static Lexing/Compiling Method
540
+ */
541
+
542
+ InlineLexer.output = function (src, links, options) {
543
+ var inline = new InlineLexer(links, options);
544
+ return inline.output(src);
545
+ };
546
+
547
+ /**
548
+ * Lexing/Compiling
549
+ */
550
+
551
+ InlineLexer.prototype.output = function (src) {
552
+ var out = ''
553
+ , link
554
+ , text
555
+ , href
556
+ , cap;
557
+
558
+ while (src) {
559
+ // escape
560
+ if (cap = this.rules.escape.exec(src)) {
561
+ src = src.substring(cap[0].length);
562
+ out += cap[1];
563
+ continue;
564
+ }
565
+
566
+ // autolink
567
+ if (cap = this.rules.autolink.exec(src)) {
568
+ src = src.substring(cap[0].length);
569
+ if (cap[2] === '@') {
570
+ text = cap[1][6] === ':'
571
+ ? this.mangle(cap[1].substring(7))
572
+ : this.mangle(cap[1]);
573
+ href = this.mangle('mailto:') + text;
574
+ } else {
575
+ text = escape(cap[1]);
576
+ href = text;
577
+ }
578
+ out += '<a href="'
579
+ + href
580
+ + '">'
581
+ + text
582
+ + '</a>';
583
+ continue;
584
+ }
585
+
586
+ // url (gfm)
587
+ if (cap = this.rules.url.exec(src)) {
588
+ src = src.substring(cap[0].length);
589
+ text = escape(cap[1]);
590
+ href = text;
591
+ out += '<a href="'
592
+ + href
593
+ + '">'
594
+ + text
595
+ + '</a>';
596
+ continue;
597
+ }
598
+
599
+ // tag
600
+ if (cap = this.rules.tag.exec(src)) {
601
+ src = src.substring(cap[0].length);
602
+ out += this.options.sanitize
603
+ ? escape(cap[0])
604
+ : cap[0];
605
+ continue;
606
+ }
607
+
608
+ // link
609
+ if (cap = this.rules.link.exec(src)) {
610
+ src = src.substring(cap[0].length);
611
+ out += this.outputLink(cap, {
612
+ href: cap[2],
613
+ title: cap[3]
614
+ });
615
+ continue;
616
+ }
617
+
618
+ // reflink, nolink
619
+ if ((cap = this.rules.reflink.exec(src))
620
+ || (cap = this.rules.nolink.exec(src))) {
621
+ src = src.substring(cap[0].length);
622
+ link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
623
+ link = this.links[link.toLowerCase()];
624
+ if (!link || !link.href) {
625
+ out += cap[0][0];
626
+ src = cap[0].substring(1) + src;
627
+ continue;
628
+ }
629
+ out += this.outputLink(cap, link);
630
+ continue;
631
+ }
632
+
633
+ // strong
634
+ if (cap = this.rules.strong.exec(src)) {
635
+ src = src.substring(cap[0].length);
636
+ out += '<strong>'
637
+ + this.output(cap[2] || cap[1])
638
+ + '</strong>';
639
+ continue;
640
+ }
641
+
642
+ // em
643
+ if (cap = this.rules.em.exec(src)) {
644
+ src = src.substring(cap[0].length);
645
+ out += '<em>'
646
+ + this.output(cap[2] || cap[1])
647
+ + '</em>';
648
+ continue;
649
+ }
650
+
651
+ // code
652
+ if (cap = this.rules.code.exec(src)) {
653
+ src = src.substring(cap[0].length);
654
+ out += '<code>'
655
+ + escape(cap[2], true)
656
+ + '</code>';
657
+ continue;
658
+ }
659
+
660
+ // br
661
+ if (cap = this.rules.br.exec(src)) {
662
+ src = src.substring(cap[0].length);
663
+ out += '<br>';
664
+ continue;
665
+ }
666
+
667
+ // del (gfm)
668
+ if (cap = this.rules.del.exec(src)) {
669
+ src = src.substring(cap[0].length);
670
+ out += '<del>'
671
+ + this.output(cap[1])
672
+ + '</del>';
673
+ continue;
674
+ }
675
+
676
+ // text
677
+ if (cap = this.rules.text.exec(src)) {
678
+ src = src.substring(cap[0].length);
679
+ out += escape(this.smartypants(cap[0]));
680
+ continue;
681
+ }
682
+
683
+ if (src) {
684
+ throw new
685
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
686
+ }
687
+ }
688
+
689
+ return out;
690
+ };
691
+
692
+ /**
693
+ * Compile Link
694
+ */
695
+
696
+ InlineLexer.prototype.outputLink = function (cap, link) {
697
+ if (cap[0][0] !== '!') {
698
+ return '<a href="'
699
+ + escape(link.href)
700
+ + '"'
701
+ + (link.title
702
+ ? ' title="'
703
+ + escape(link.title)
704
+ + '"'
705
+ : '')
706
+ + '>'
707
+ + this.output(cap[1])
708
+ + '</a>';
709
+ } else {
710
+ return '<img src="'
711
+ + escape(link.href)
712
+ + '" alt="'
713
+ + escape(cap[1])
714
+ + '"'
715
+ + (link.title
716
+ ? ' title="'
717
+ + escape(link.title)
718
+ + '"'
719
+ : '')
720
+ + '>';
721
+ }
722
+ };
723
+
724
+ /**
725
+ * Smartypants Transformations
726
+ */
727
+
728
+ InlineLexer.prototype.smartypants = function (text) {
729
+ if (!this.options.smartypants) return text;
730
+ return text
731
+ .replace(/--/g, '\u2014')
732
+ .replace(/'([^']*)'/g, '\u2018$1\u2019')
733
+ .replace(/"([^"]*)"/g, '\u201C$1\u201D')
734
+ .replace(/\.{3}/g, '\u2026');
735
+ };
736
+
737
+ /**
738
+ * Mangle Links
739
+ */
740
+
741
+ InlineLexer.prototype.mangle = function (text) {
742
+ var out = ''
743
+ , l = text.length
744
+ , i = 0
745
+ , ch;
746
+
747
+ for (; i < l; i++) {
748
+ ch = text.charCodeAt(i);
749
+ if (Math.random() > 0.5) {
750
+ ch = 'x' + ch.toString(16);
751
+ }
752
+ out += '&#' + ch + ';';
753
+ }
754
+
755
+ return out;
756
+ };
757
+
758
+ /**
759
+ * Parsing & Compiling
760
+ */
761
+
762
+ function Parser(options) {
763
+ this.tokens = [];
764
+ this.token = null;
765
+ this.options = options || marked.defaults;
766
+ }
767
+
768
+ /**
769
+ * Static Parse Method
770
+ */
771
+
772
+ Parser.parse = function (src, options) {
773
+ var parser = new Parser(options);
774
+ return parser.parse(src);
775
+ };
776
+
777
+ /**
778
+ * Parse Loop
779
+ */
780
+
781
+ Parser.prototype.parse = function (src) {
782
+ this.inline = new InlineLexer(src.links, this.options);
783
+ this.tokens = src.reverse();
784
+
785
+ var out = '';
786
+ while (this.next()) {
787
+ out += this.tok();
788
+ }
789
+
790
+ return out;
791
+ };
792
+
793
+ /**
794
+ * Next Token
795
+ */
796
+
797
+ Parser.prototype.next = function () {
798
+ return this.token = this.tokens.pop();
799
+ };
800
+
801
+ /**
802
+ * Preview Next Token
803
+ */
804
+
805
+ Parser.prototype.peek = function () {
806
+ return this.tokens[this.tokens.length - 1] || 0;
807
+ };
808
+
809
+ /**
810
+ * Parse Text Tokens
811
+ */
812
+
813
+ Parser.prototype.parseText = function () {
814
+ var body = this.token.text;
815
+
816
+ while (this.peek().type === 'text') {
817
+ body += '\n' + this.next().text;
818
+ }
819
+
820
+ return this.inline.output(body);
821
+ };
822
+
823
+ /**
824
+ * Parse Current Token
825
+ */
826
+
827
+ Parser.prototype.tok = function () {
828
+ switch (this.token.type) {
829
+ case 'space':
830
+ {
831
+ return '';
832
+ }
833
+ case 'hr':
834
+ {
835
+ return '<hr>\n';
836
+ }
837
+ case 'heading':
838
+ {
839
+ return '<h'
840
+ + this.token.depth
841
+ + '>'
842
+ + this.inline.output(this.token.text)
843
+ + '</h'
844
+ + this.token.depth
845
+ + '>\n';
846
+ }
847
+ case 'code':
848
+ {
849
+ if (this.options.highlight) {
850
+ var code = this.options.highlight(this.token.text, this.token.lang);
851
+ if (code != null && code !== this.token.text) {
852
+ this.token.escaped = true;
853
+ this.token.text = code;
854
+ }
855
+ }
856
+
857
+ if (!this.token.escaped) {
858
+ this.token.text = escape(this.token.text, true);
859
+ }
860
+
861
+ return '<pre><code'
862
+ + (this.token.lang
863
+ ? ' class="'
864
+ + this.options.langPrefix
865
+ + this.token.lang
866
+ + '"'
867
+ : '')
868
+ + '>'
869
+ + this.token.text
870
+ + '</code></pre>\n';
871
+ }
872
+ case 'table':
873
+ {
874
+ var body = ''
875
+ , heading
876
+ , i
877
+ , row
878
+ , cell
879
+ , j;
880
+
881
+ // header
882
+ body += '<thead>\n<tr>\n';
883
+ for (i = 0; i < this.token.header.length; i++) {
884
+ heading = this.inline.output(this.token.header[i]);
885
+ body += this.token.align[i]
886
+ ? '<th align="' + this.token.align[i] + '">' + heading + '</th>\n'
887
+ : '<th>' + heading + '</th>\n';
888
+ }
889
+ body += '</tr>\n</thead>\n';
890
+
891
+ // body
892
+ body += '<tbody>\n'
893
+ for (i = 0; i < this.token.cells.length; i++) {
894
+ row = this.token.cells[i];
895
+ body += '<tr>\n';
896
+ for (j = 0; j < row.length; j++) {
897
+ cell = this.inline.output(row[j]);
898
+ body += this.token.align[j]
899
+ ? '<td align="' + this.token.align[j] + '">' + cell + '</td>\n'
900
+ : '<td>' + cell + '</td>\n';
901
+ }
902
+ body += '</tr>\n';
903
+ }
904
+ body += '</tbody>\n';
905
+
906
+ return '<table>\n'
907
+ + body
908
+ + '</table>\n';
909
+ }
910
+ case 'blockquote_start':
911
+ {
912
+ var body = '';
913
+
914
+ while (this.next().type !== 'blockquote_end') {
915
+ body += this.tok();
916
+ }
917
+
918
+ return '<blockquote>\n'
919
+ + body
920
+ + '</blockquote>\n';
921
+ }
922
+ case 'list_start':
923
+ {
924
+ var type = this.token.ordered ? 'ol' : 'ul'
925
+ , body = '';
926
+
927
+ while (this.next().type !== 'list_end') {
928
+ body += this.tok();
929
+ }
930
+
931
+ return '<'
932
+ + type
933
+ + '>\n'
934
+ + body
935
+ + '</'
936
+ + type
937
+ + '>\n';
938
+ }
939
+ case 'list_item_start':
940
+ {
941
+ var body = '';
942
+
943
+ while (this.next().type !== 'list_item_end') {
944
+ body += this.token.type === 'text'
945
+ ? this.parseText()
946
+ : this.tok();
947
+ }
948
+
949
+ return '<li>'
950
+ + body
951
+ + '</li>\n';
952
+ }
953
+ case 'loose_item_start':
954
+ {
955
+ var body = '';
956
+
957
+ while (this.next().type !== 'list_item_end') {
958
+ body += this.tok();
959
+ }
960
+
961
+ return '<li>'
962
+ + body
963
+ + '</li>\n';
964
+ }
965
+ case 'html':
966
+ {
967
+ return !this.token.pre && !this.options.pedantic
968
+ ? this.inline.output(this.token.text)
969
+ : this.token.text;
970
+ }
971
+ case 'paragraph':
972
+ {
973
+ return '<p>'
974
+ + this.inline.output(this.token.text)
975
+ + '</p>\n';
976
+ }
977
+ case 'text':
978
+ {
979
+ return '<p>'
980
+ + this.parseText()
981
+ + '</p>\n';
982
+ }
983
+ }
984
+ };
985
+
986
+ /**
987
+ * Helpers
988
+ */
989
+
990
+ function escape(html, encode) {
991
+ return html
992
+ .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
993
+ .replace(/</g, '&lt;')
994
+ .replace(/>/g, '&gt;')
995
+ .replace(/"/g, '&quot;')
996
+ .replace(/'/g, '&#39;');
997
+ }
998
+
999
+ function replace(regex, opt) {
1000
+ regex = regex.source;
1001
+ opt = opt || '';
1002
+ return function self(name, val) {
1003
+ if (!name) return new RegExp(regex, opt);
1004
+ val = val.source || val;
1005
+ val = val.replace(/(^|[^\[])\^/g, '$1');
1006
+ regex = regex.replace(name, val);
1007
+ return self;
1008
+ };
1009
+ }
1010
+
1011
+ function noop() {
1012
+ }
1013
+
1014
+ noop.exec = noop;
1015
+
1016
+ function merge(obj) {
1017
+ var i = 1
1018
+ , target
1019
+ , key;
1020
+
1021
+ for (; i < arguments.length; i++) {
1022
+ target = arguments[i];
1023
+ for (key in target) {
1024
+ if (Object.prototype.hasOwnProperty.call(target, key)) {
1025
+ obj[key] = target[key];
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ return obj;
1031
+ }
1032
+
1033
+ /**
1034
+ * Marked
1035
+ */
1036
+
1037
+ function marked(src, opt, callback) {
1038
+ if (callback || typeof opt === 'function') {
1039
+ if (!callback) {
1040
+ callback = opt;
1041
+ opt = null;
1042
+ }
1043
+
1044
+ if (opt) opt = merge({}, marked.defaults, opt);
1045
+
1046
+ var highlight = opt.highlight
1047
+ , tokens
1048
+ , pending
1049
+ , i = 0;
1050
+
1051
+ try {
1052
+ tokens = Lexer.lex(src, opt)
1053
+ } catch (e) {
1054
+ return callback(e);
1055
+ }
1056
+
1057
+ pending = tokens.length;
1058
+
1059
+ var done = function (hi) {
1060
+ var out, err;
1061
+
1062
+ if (hi !== true) {
1063
+ delete opt.highlight;
1064
+ }
1065
+
1066
+ try {
1067
+ out = Parser.parse(tokens, opt);
1068
+ } catch (e) {
1069
+ err = e;
1070
+ }
1071
+
1072
+ opt.highlight = highlight;
1073
+
1074
+ return err
1075
+ ? callback(err)
1076
+ : callback(null, out);
1077
+ };
1078
+
1079
+ if (!highlight || highlight.length < 3) {
1080
+ return done(true);
1081
+ }
1082
+
1083
+ if (!pending) return done();
1084
+
1085
+ for (; i < tokens.length; i++) {
1086
+ (function (token) {
1087
+ if (token.type !== 'code') {
1088
+ return --pending || done();
1089
+ }
1090
+ return highlight(token.text, token.lang, function (err, code) {
1091
+ if (code == null || code === token.text) {
1092
+ return --pending || done();
1093
+ }
1094
+ token.text = code;
1095
+ token.escaped = true;
1096
+ --pending || done();
1097
+ });
1098
+ })(tokens[i]);
1099
+ }
1100
+
1101
+ return;
1102
+ }
1103
+ try {
1104
+ if (opt) opt = merge({}, marked.defaults, opt);
1105
+ return Parser.parse(Lexer.lex(src, opt), opt);
1106
+ } catch (e) {
1107
+ e.message += '\nPlease report this to https://github.com/chjj/marked.';
1108
+ if ((opt || marked.defaults).silent) {
1109
+ return '<p>An error occured:</p><pre>'
1110
+ + escape(e.message + '', true)
1111
+ + '</pre>';
1112
+ }
1113
+ throw e;
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Options
1119
+ */
1120
+
1121
+ marked.options =
1122
+ marked.setOptions = function (opt) {
1123
+ merge(marked.defaults, opt);
1124
+ return marked;
1125
+ };
1126
+
1127
+ marked.defaults = {
1128
+ gfm: true,
1129
+ tables: true,
1130
+ breaks: false,
1131
+ pedantic: false,
1132
+ sanitize: false,
1133
+ smartLists: false,
1134
+ silent: false,
1135
+ highlight: null,
1136
+ langPrefix: 'lang-',
1137
+ smartypants: false
1138
+ };
1139
+
1140
+ /**
1141
+ * Expose
1142
+ */
1143
+
1144
+ marked.Parser = Parser;
1145
+ marked.parser = Parser.parse;
1146
+
1147
+ marked.Lexer = Lexer;
1148
+ marked.lexer = Lexer.lex;
1149
+
1150
+ marked.InlineLexer = InlineLexer;
1151
+ marked.inlineLexer = InlineLexer.output;
1152
+
1153
+ marked.parse = marked;
1154
+
1155
+ if (typeof exports === 'object') {
1156
+ module.exports = marked;
1157
+ } else if (typeof define === 'function' && define.amd) {
1158
+ define(function () {
1159
+ return marked;
1160
+ });
1161
+ } else {
1162
+ this.marked = marked;
1163
+ }
1164
+
1165
+ }).call(function () {
1166
+ return this || (typeof window !== 'undefined' ? window : global);
1167
+ }());
package.xml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>markdown</name>
4
+ <version>1.1.0</version>
5
+ <stability>stable</stability>
6
+ <license uri="https://github.com/SchumacherFM/Magento-Markdown">Custom</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>Markdown as module for Magento! Replaces the TinyMCE editor. Markdown is a text-to-HTML conversion tool for web writers. Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML).</summary>
10
+ <description>Markdown is a text-to-HTML conversion tool for web writers. Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML).&#xD;
11
+ &#xD;
12
+ Full documentation of Markdown's syntax is available on John's Markdown page: http://daringfireball.net/projects/markdown/&#xD;
13
+ &#xD;
14
+ Full support of Markdown Extra: http://michelf.ca/projects/php-markdown/extra/&#xD;
15
+ &#xD;
16
+ This module renders all CMS pages and every block which extends Mage_Core_Block_Abstract.&#xD;
17
+ &#xD;
18
+ Rendering of catalog description fields have to be implemented in the phtml files by yourself.&#xD;
19
+ &#xD;
20
+ Preview in the backend. No live preview available maybe later.</description>
21
+ <notes>https://github.com/SchumacherFM/Magento-Markdown</notes>
22
+ <authors><author><name>Cyrill Schumacher</name><user>cyrills</user><email>cyrill@schumacher.fm</email></author></authors>
23
+ <date>2013-09-02</date>
24
+ <time>11:14:49</time>
25
+ <contents><target name="magecommunity"><dir name="SchumacherFM"><dir name="Markdown"><dir><dir name="Helper"><file name="Data.php" hash="f9d8f27ac5f1080d7756284757833e37"/></dir><dir name="Model"><dir name="Editor"><file name="Config.php" hash="8d7361dbb53065a36cc2aaf4899107f2"/><file name="Observer.php" hash="ad29b486cbe198b89e04db266d10c16b"/></dir><dir name="Markdown"><file name="Render.php" hash="f077f2f523d932c9b8e6100403afe511"/></dir><dir name="Michelf"><dir name="Markdown"><file name="Extra.php" hash="aaee7a66298007f8c003ba96d53deab9"/><file name="TmpImpl.php" hash="5ec68c28519d8ecb4a43324f5f197a80"/></dir><file name="Markdown.php" hash="bb4d4c2b2b0c18d1b2970c1afbf110a4"/></dir><dir name="Observer"><file name="AdminhtmlBlock.php" hash="9b2464dba37e1cf829503d03e7477ad3"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="82b1eef25a4af5d8499deb8350a729bb"/><file name="config.xml" hash="05f418947ca0509a8fdb5477b936cff1"/><file name="system.xml" hash="e4ae8261f23405dcee3985070896cb08"/></dir><dir name="sql"><dir name="markdown_setup"><file name="install-1.0.0.php" hash="0fe5a9650dd224f691caf585a34c52e6"/></dir></dir></dir><file name=".DS_Store" hash="29236c1e2932a8a54035bfbe9e734f49"/></dir></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="markdown.xml" hash="43a7d9575776d74fc3dbd185445729f8"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="SchumacherFM_Markdown.xml" hash="db8742448ef30a5af1bd60865f429c26"/></dir></target><target name="magelocale"><dir><dir name="de_DE"><file name="SchumacherFM_Markdown.csv" hash="d41d8cd98f00b204e9800998ecf8427e"/></dir><dir name="en_US"><file name="SchumacherFM_Markdown.csv" hash="d41d8cd98f00b204e9800998ecf8427e"/></dir></dir></target><target name="mage"><dir name="js"><dir name="mage"><dir name="adminhtml"><file name="markdown.js" hash="83c9424fcfebbebd30ebaa010ed214b6"/><file name="marked.js" hash="1fa57589a791297ce906795692f0252e"/></dir></dir></dir></target><target name="mageskin"><dir name="adminhtml"><dir name="default"><dir name="default"><file name="markdown.css" hash="68699f62ea8666484819346ff143ee88"/></dir></dir></dir></target></contents>
26
+ <compatible/>
27
+ <dependencies><required><php><min>5.2.0</min><max>6.0.0</max></php></required></dependencies>
28
+ </package>
skin/adminhtml/default/default/markdown.css ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright http://kevinburke.bitbucket.org/markdowncss/
3
+ Adapted for Magento Markdown
4
+ */
5
+ .markdown {
6
+ margin : 0 auto;
7
+ font-family : Georgia, Palatino, serif;
8
+ color : #444444;
9
+ line-height : 1;
10
+ max-width : 960px;
11
+ padding : 30px;
12
+ background-color : white;
13
+ }
14
+
15
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4 {
16
+ color : #111111;
17
+ font-weight : 400;
18
+ }
19
+
20
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown p {
21
+ margin-bottom : 24px;
22
+ padding : 0;
23
+ }
24
+
25
+ .markdown h1 {
26
+ font-size : 48px;
27
+ }
28
+
29
+ .markdown h2 {
30
+ font-size : 36px;
31
+ /* The bottom margin is small. It's designed to be used with gray meta text
32
+ * below a post title. */
33
+ margin : 24px 0 6px;
34
+ }
35
+
36
+ .markdown h3 {
37
+ font-size : 24px;
38
+ }
39
+
40
+ .markdown h4 {
41
+ font-size : 21px;
42
+ }
43
+
44
+ .markdown h5 {
45
+ font-size : 18px;
46
+ }
47
+
48
+ .markdown a {
49
+ color : #0099ff;
50
+ margin : 0;
51
+ padding : 0;
52
+ vertical-align : baseline;
53
+ }
54
+
55
+ .markdown a:hover {
56
+ text-decoration : none;
57
+ color : #ff6600;
58
+ }
59
+
60
+ .markdown a:visited {
61
+ color : purple;
62
+ }
63
+
64
+ .markdown ul, ol {
65
+ list-style: disc inside;
66
+ padding : 0;
67
+ margin : 0;
68
+ }
69
+
70
+ .markdown li {
71
+ line-height : 24px;
72
+ }
73
+
74
+ .markdown li ul, .markdown li ul {
75
+ margin-left : 24px;
76
+ }
77
+
78
+ .markdown p, .markdown ul, .markdown ol {
79
+ font-size : 16px;
80
+ line-height : 24px;
81
+ /*max-width : 540px;*/
82
+ }
83
+
84
+ .markdown pre {
85
+ padding : 0px 24px;
86
+ max-width : 700px;
87
+ white-space : pre-wrap;
88
+ }
89
+
90
+ .markdown code, .markdown pre code {
91
+ font-family : Consolas, Monaco, Andale Mono, monospace;
92
+ line-height : 1.5;
93
+ font-size : 13px;
94
+
95
+ margin : 0 2px;
96
+ padding : 0 5px;
97
+ /*white-space: nowrap;*/
98
+ border : 1px solid #eaeaea;
99
+ background-color : #f8f8f8;
100
+ border-radius : 3px;
101
+ }
102
+
103
+ .markdown aside {
104
+ display : block;
105
+ float : right;
106
+ width : 390px;
107
+ }
108
+
109
+ .markdown blockquote {
110
+ border-left : .5em solid #eee;
111
+ padding : 0 2em;
112
+ margin-left : 0;
113
+ max-width : 476px;
114
+ }
115
+
116
+ .markdown blockquote cite {
117
+ font-size : 14px;
118
+ line-height : 20px;
119
+ color : #bfbfbf;
120
+ }
121
+
122
+ .markdown blockquote cite:before {
123
+ content : '\2014 \00A0';
124
+ }
125
+
126
+ .markdown blockquote p {
127
+ color : #666;
128
+ max-width : 460px;
129
+ }
130
+
131
+ .markdown hr {
132
+ width : 540px;
133
+ text-align : left;
134
+ margin : 0 auto 0 0;
135
+ color : #999;
136
+ }
137
+
138
+ /* Code below this line is copyright Twitter Inc. */
139
+
140
+ .markdown button,
141
+ .markdown input,
142
+ .markdown select,
143
+ .markdown textarea {
144
+ font-size : 100%;
145
+ margin : 0;
146
+ vertical-align : baseline;
147
+ *vertical-align : middle;
148
+ }
149
+
150
+ .markdown button, .markdown input {
151
+ line-height : normal;
152
+ *overflow : visible;
153
+ }
154
+
155
+ .markdown button::-moz-focus-inner, .markdown input::-moz-focus-inner {
156
+ border : 0;
157
+ padding : 0;
158
+ }
159
+
160
+ .markdown button,
161
+ .markdown input[type="button"],
162
+ .markdown input[type="reset"],
163
+ .markdown input[type="submit"] {
164
+ cursor : pointer;
165
+ -webkit-appearance : button;
166
+ }
167
+
168
+ .markdown input[type=checkbox], .markdown input[type=radio] {
169
+ cursor : pointer;
170
+ }
171
+
172
+ /* override default chrome & firefox settings */
173
+ .markdown input:not([type="image"]), .markdown textarea {
174
+ -webkit-box-sizing : content-box;
175
+ -moz-box-sizing : content-box;
176
+ box-sizing : content-box;
177
+ }
178
+
179
+ .markdown input[type="search"] {
180
+ -webkit-appearance : textfield;
181
+ -webkit-box-sizing : content-box;
182
+ -moz-box-sizing : content-box;
183
+ box-sizing : content-box;
184
+ }
185
+
186
+ .markdown input[type="search"]::-webkit-search-decoration {
187
+ -webkit-appearance : none;
188
+ }
189
+
190
+ .markdown label,
191
+ .markdown input,
192
+ .markdown select,
193
+ .markdown textarea {
194
+ font-family : "Helvetica Neue", Helvetica, Arial, sans-serif;
195
+ font-size : 13px;
196
+ font-weight : normal;
197
+ line-height : normal;
198
+ margin-bottom : 18px;
199
+ }
200
+
201
+ .markdown input[type=checkbox], .markdown input[type=radio] {
202
+ cursor : pointer;
203
+ margin-bottom : 0;
204
+ }
205
+
206
+ .markdown input[type=text],
207
+ .markdown input[type=password],
208
+ .markdown textarea,
209
+ .markdown select {
210
+ display : inline-block;
211
+ width : 210px;
212
+ padding : 4px;
213
+ font-size : 13px;
214
+ font-weight : normal;
215
+ line-height : 18px;
216
+ height : 18px;
217
+ color : #808080;
218
+ border : 1px solid #ccc;
219
+ -webkit-border-radius : 3px;
220
+ -moz-border-radius : 3px;
221
+ border-radius : 3px;
222
+ }
223
+
224
+ .markdown select, .markdown input[type=file] {
225
+ height : 27px;
226
+ line-height : 27px;
227
+ }
228
+
229
+ .markdown textarea {
230
+ height : auto;
231
+ }
232
+
233
+ /* grey out placeholders */
234
+ .markdown :-moz-placeholder {
235
+ color : #bfbfbf;
236
+ }
237
+
238
+ .markdown ::-webkit-input-placeholder {
239
+ color : #bfbfbf;
240
+ }
241
+
242
+ .markdown input[type=text],
243
+ .markdown input[type=password],
244
+ .markdown select,
245
+ .markdown textarea {
246
+ -webkit-transition : border linear 0.2s, box-shadow linear 0.2s;
247
+ -moz-transition : border linear 0.2s, box-shadow linear 0.2s;
248
+ transition : border linear 0.2s, box-shadow linear 0.2s;
249
+ -webkit-box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1);
250
+ -moz-box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1);
251
+ box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1);
252
+ }
253
+
254
+ .markdown input[type=text]:focus, .markdown input[type=password]:focus, .markdown textarea:focus {
255
+ outline : none;
256
+ border-color : rgba(82, 168, 236, 0.8);
257
+ -webkit-box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
258
+ -moz-box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
259
+ box-shadow : inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
260
+ }
261
+
262
+ /* buttons */
263
+ .markdown button {
264
+ display : inline-block;
265
+ padding : 4px 14px;
266
+ font-family : "Helvetica Neue", Helvetica, Arial, sans-serif;
267
+ font-size : 13px;
268
+ line-height : 18px;
269
+ -webkit-border-radius : 4px;
270
+ -moz-border-radius : 4px;
271
+ border-radius : 4px;
272
+ -webkit-box-shadow : inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
273
+ -moz-box-shadow : inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
274
+ box-shadow : inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
275
+ background-color : #0064cd;
276
+ background-repeat : repeat-x;
277
+ background-image : -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
278
+ background-image : -moz-linear-gradient(top, #049cdb, #0064cd);
279
+ background-image : -ms-linear-gradient(top, #049cdb, #0064cd);
280
+ background-image : -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
281
+ background-image : -webkit-linear-gradient(top, #049cdb, #0064cd);
282
+ background-image : -o-linear-gradient(top, #049cdb, #0064cd);
283
+ background-image : linear-gradient(top, #049cdb, #0064cd);
284
+ color : #fff;
285
+ text-shadow : 0 -1px 0 rgba(0, 0, 0, 0.25);
286
+ border : 1px solid #004b9a;
287
+ border-bottom-color : #003f81;
288
+ -webkit-transition : 0.1s linear all;
289
+ -moz-transition : 0.1s linear all;
290
+ transition : 0.1s linear all;
291
+ border-color : #0064cd #0064cd #003f81;
292
+ border-color : rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
293
+ }
294
+
295
+ .markdown button:hover {
296
+ color : #fff;
297
+ background-position : 0 -15px;
298
+ text-decoration : none;
299
+ }
300
+
301
+ .markdown button:active {
302
+ -webkit-box-shadow : inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
303
+ -moz-box-shadow : inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
304
+ box-shadow : inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
305
+ }
306
+
307
+ .markdown button::-moz-focus-inner {
308
+ padding : 0;
309
+ border : 0;
310
+ }