├── .gitattributes
├── MyMarkdown.php
├── README.md
├── cebe-markdown
├── GithubMarkdown.php
├── Markdown.php
├── MarkdownExtra.php
├── Parser.php
├── block
│ ├── CodeTrait.php
│ ├── FencedCodeTrait.php
│ ├── HeadlineTrait.php
│ ├── HtmlTrait.php
│ ├── ListTrait.php
│ ├── QuoteTrait.php
│ ├── RuleTrait.php
│ └── TableTrait.php
└── inline
│ ├── CodeTrait.php
│ ├── EmphStrongTrait.php
│ ├── LinkTrait.php
│ ├── StrikeoutTrait.php
│ └── UrlLinkTrait.php
├── metadata.json
├── pagedown
├── LICENSE.txt
├── Markdown.Converter.js
├── Markdown.Editor.js
├── Markdown.Sanitizer.js
├── highlight.min.js
├── highlightjs-run.js
├── highlightjs.css
├── markdown.min.js
├── wmd-buttons.png
└── wmd.css
├── qa-markdown-editor.php
├── qa-markdown-upload.php
├── qa-markdown-viewer.php
├── qa-md-events.php
├── qa-md-lang-default.php
├── qa-md-layer.php
└── qa-plugin.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/MyMarkdown.php:
--------------------------------------------------------------------------------
1 | "
\n", "\n" => "
\n"]);
13 | }
14 |
15 | /**
16 | * Update headings to run from 2..6 to avoid multiple H1s on the page
17 | */
18 | protected function renderHeadline($block)
19 | {
20 | $tag = 'h' . min($block['level'] + 1, 6);
21 | return "<$tag>" . $this->renderAbsy($block['content']) . "$tag>\n";
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Markdown Editor plugin for Question2Answer
2 | =================================================
3 |
4 | This is an editor plugin for popular open source Q&A platform, [Question2Answer](http://www.question2answer.org). It uses Markdown to format posts, which is a simple text-friendly markup language using for example \*\*bold\*\* for **bold text** or \> for quoting sources.
5 |
6 | The plugin uses modified versions of the PageDown scripts (released by Stack Overflow) for the editor and live preview respectively.
7 |
8 |
9 | Installation
10 | -------------------------------------------------
11 |
12 | 1. Download and extract the `markdown-editor` folder to the `qa-plugins` folder in your Q2A installation.
13 | 2. If your site is a different language from English, copy `qa-md-lang-default.php` to the required language code (e.g. `qa-tt-lang-de.php` for German) and edit the phrases for your language.
14 | 3. Log in to your Q2A site as a Super Administrator and head to Admin > Posting.
15 | 4. Set the default editor for questions and answers to 'Markdown Editor'. The editor does also work for comments, but keeping to plain text is recommended.
16 |
17 | In Admin > Plugins, you can set three options:
18 |
19 | - "Don't add CSS inline" - this will not output the CSS onto the page, to allow putting it in a stylesheet instead which is more efficient. Copy the CSS from `pagedown/wmd.css` to the bottom of your theme's current stylesheet.
20 | - "Plaintext comments" - Sets a post as plaintext when converting answers to comments.
21 | - "Use syntax highlighting" - Integrates [highlight.js](http://softwaremaniacs.org/soft/highlight/en/) for code blocks (including while writing posts). All common programming languages are supported, but you can add more using the [customized download here](http://softwaremaniacs.org/soft/highlight/en/download/). Save the file and overwrite `pagedown/highlight.min.js`. If you ticked the box for CSS above, copy the CSS from `pagedown/highlightjs.css` to the bottom of your theme's current stylesheet.
22 |
23 |
24 | Extra bits
25 | -------------------------------------------------
26 |
27 | **Converting old posts:** If you have been running your Q2A site for a little while, you may wish to convert old content to Markdown. This does not work reliably for HTML content (created via the WYSIWYG editor); it is pretty safe for plain text content, but check your posts afterwards as some formatting may go awry. You can convert text posts automatically using this SQL query:
28 |
29 | UPDATE qa_posts SET format='markdown' WHERE format='' AND type IN ('Q', 'A', 'Q_HIDDEN', 'A_HIDDEN')
30 |
31 | (Make sure to change `qa_` above to your installation's table prefix if it is different.)
32 |
33 |
34 | Pay What You Like
35 | -------------------------------------------------
36 |
37 | Most of my code is released under the open source GPLv3 license, and provided with a 'Pay What You Like' approach. Feel free to download and modify the plugins/themes to suit your needs, and I hope you value them enough to donate a few dollars.
38 |
39 | ### [Donate here](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4R5SHBNM3UDLU)
40 |
--------------------------------------------------------------------------------
/cebe-markdown/GithubMarkdown.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class GithubMarkdown extends Markdown
16 | {
17 | // include block element parsing using traits
18 | use block\TableTrait;
19 | use block\FencedCodeTrait;
20 |
21 | // include inline element parsing using traits
22 | use inline\StrikeoutTrait;
23 | use inline\UrlLinkTrait;
24 |
25 | /**
26 | * @var boolean whether to interpret newlines as `
`-tags.
27 | * This feature is useful for comments where newlines are often meant to be real new lines.
28 | */
29 | public $enableNewlines = false;
30 |
31 | /**
32 | * @inheritDoc
33 | */
34 | protected $escapeCharacters = [
35 | // from Markdown
36 | '\\', // backslash
37 | '`', // backtick
38 | '*', // asterisk
39 | '_', // underscore
40 | '{', '}', // curly braces
41 | '[', ']', // square brackets
42 | '(', ')', // parentheses
43 | '#', // hash mark
44 | '+', // plus sign
45 | '-', // minus sign (hyphen)
46 | '.', // dot
47 | '!', // exclamation mark
48 | '<', '>',
49 | // added by GithubMarkdown
50 | ':', // colon
51 | '|', // pipe
52 | ];
53 |
54 |
55 |
56 | /**
57 | * Consume lines for a paragraph
58 | *
59 | * Allow headlines, lists and code to break paragraphs
60 | */
61 | protected function consumeParagraph($lines, $current)
62 | {
63 | // consume until newline
64 | $content = [];
65 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
66 | $line = $lines[$i];
67 | if ($line === ''
68 | || ltrim($line) === ''
69 | || !ctype_alpha($line[0]) && (
70 | $this->identifyQuote($line, $lines, $i) ||
71 | $this->identifyFencedCode($line, $lines, $i) ||
72 | $this->identifyUl($line, $lines, $i) ||
73 | $this->identifyOl($line, $lines, $i) ||
74 | $this->identifyHr($line, $lines, $i)
75 | )
76 | || $this->identifyHeadline($line, $lines, $i))
77 | {
78 | break;
79 | } elseif ($this->identifyCode($line, $lines, $i)) {
80 | // possible beginning of a code block
81 | // but check for continued inline HTML
82 | // e.g.
84 | if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
85 | $content[] = $line;
86 | } else {
87 | break;
88 | }
89 | } else {
90 | $content[] = $line;
91 | }
92 | }
93 | $block = [
94 | 'paragraph',
95 | 'content' => $this->parseInline(implode("\n", $content)),
96 | ];
97 | return [$block, --$i];
98 | }
99 |
100 | /**
101 | * @inheritdocs
102 | *
103 | * Parses a newline indicated by two spaces on the end of a markdown line.
104 | */
105 | protected function renderText($text)
106 | {
107 | if ($this->enableNewlines) {
108 | $br = $this->html5 ? "
\n" : "
\n";
109 | return strtr($text[1], [" \n" => $br, "\n" => $br]);
110 | } else {
111 | return parent::renderText($text);
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/cebe-markdown/Markdown.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class Markdown extends Parser
16 | {
17 | // include block element parsing using traits
18 | use block\CodeTrait;
19 | use block\HeadlineTrait;
20 | use block\HtmlTrait {
21 | parseInlineHtml as private;
22 | }
23 | use block\ListTrait {
24 | // Check Ul List before headline
25 | identifyUl as protected identifyBUl;
26 | consumeUl as protected consumeBUl;
27 | }
28 | use block\QuoteTrait;
29 | use block\RuleTrait {
30 | // Check Hr before checking lists
31 | identifyHr as protected identifyAHr;
32 | consumeHr as protected consumeAHr;
33 | }
34 |
35 | // include inline element parsing using traits
36 | use inline\CodeTrait;
37 | use inline\EmphStrongTrait;
38 | use inline\LinkTrait;
39 |
40 | /**
41 | * @var boolean whether to format markup according to HTML5 spec.
42 | * Defaults to `false` which means that markup is formatted as HTML4.
43 | */
44 | public $html5 = false;
45 |
46 | /**
47 | * @var array these are "escapeable" characters. When using one of these prefixed with a
48 | * backslash, the character will be outputted without the backslash and is not interpreted
49 | * as markdown.
50 | */
51 | protected $escapeCharacters = [
52 | '\\', // backslash
53 | '`', // backtick
54 | '*', // asterisk
55 | '_', // underscore
56 | '{', '}', // curly braces
57 | '[', ']', // square brackets
58 | '(', ')', // parentheses
59 | '#', // hash mark
60 | '+', // plus sign
61 | '-', // minus sign (hyphen)
62 | '.', // dot
63 | '!', // exclamation mark
64 | '<', '>',
65 | ];
66 |
67 |
68 | /**
69 | * @inheritDoc
70 | */
71 | protected function prepare()
72 | {
73 | // reset references
74 | $this->references = [];
75 | }
76 |
77 | /**
78 | * Consume lines for a paragraph
79 | *
80 | * Allow headlines and code to break paragraphs
81 | */
82 | protected function consumeParagraph($lines, $current)
83 | {
84 | // consume until newline
85 | $content = [];
86 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
87 | $line = $lines[$i];
88 |
89 | // a list may break a paragraph when it is inside of a list
90 | if (isset($this->context[1]) && $this->context[1] === 'list' && !ctype_alpha($line[0]) && (
91 | $this->identifyUl($line, $lines, $i) || $this->identifyOl($line, $lines, $i))) {
92 | break;
93 | }
94 |
95 | if ($line === '' || ltrim($line) === '' || $this->identifyHeadline($line, $lines, $i)) {
96 | break;
97 | } elseif ($line[0] === "\t" || $line[0] === " " && strncmp($line, ' ', 4) === 0) {
98 | // possible beginning of a code block
99 | // but check for continued inline HTML
100 | // e.g.
102 | if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
103 | $content[] = $line;
104 | } else {
105 | break;
106 | }
107 | } else {
108 | $content[] = $line;
109 | }
110 | }
111 | $block = [
112 | 'paragraph',
113 | 'content' => $this->parseInline(implode("\n", $content)),
114 | ];
115 | return [$block, --$i];
116 | }
117 |
118 |
119 | /**
120 | * @inheritdocs
121 | *
122 | * Parses a newline indicated by two spaces on the end of a markdown line.
123 | */
124 | protected function renderText($text)
125 | {
126 | return str_replace(" \n", $this->html5 ? "
\n" : "
\n", $text[1]);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/cebe-markdown/MarkdownExtra.php:
--------------------------------------------------------------------------------
1 |
14 | * @license https://github.com/cebe/markdown/blob/master/LICENSE
15 | * @link https://github.com/cebe/markdown#readme
16 | */
17 | class MarkdownExtra extends Markdown
18 | {
19 | // include block element parsing using traits
20 | use block\TableTrait;
21 | use block\FencedCodeTrait;
22 |
23 | // include inline element parsing using traits
24 | // TODO
25 |
26 | /**
27 | * @var bool whether special attributes on code blocks should be applied on the `
` element.
28 | * The default behavior is to put them on the `` element.
29 | */
30 | public $codeAttributesOnPre = false;
31 |
32 | /**
33 | * @inheritDoc
34 | */
35 | protected $escapeCharacters = [
36 | // from Markdown
37 | '\\', // backslash
38 | '`', // backtick
39 | '*', // asterisk
40 | '_', // underscore
41 | '{', '}', // curly braces
42 | '[', ']', // square brackets
43 | '(', ')', // parentheses
44 | '#', // hash mark
45 | '+', // plus sign
46 | '-', // minus sign (hyphen)
47 | '.', // dot
48 | '!', // exclamation mark
49 | '<', '>',
50 | // added by MarkdownExtra
51 | ':', // colon
52 | '|', // pipe
53 | ];
54 |
55 | private $_specialAttributesRegex = '\{(([#\.][A-z0-9-_]+\s*)+)\}';
56 |
57 | // TODO allow HTML intended 3 spaces
58 |
59 | // TODO add markdown inside HTML blocks
60 |
61 | // TODO implement definition lists
62 |
63 | // TODO implement footnotes
64 |
65 | // TODO implement Abbreviations
66 |
67 |
68 | // block parsing
69 |
70 | protected function identifyReference($line)
71 | {
72 | return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $line);
73 | }
74 |
75 | /**
76 | * Consume link references
77 | */
78 | protected function consumeReference($lines, $current)
79 | {
80 | while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $lines[$current], $matches)) {
81 | $label = strtolower($matches[1]);
82 |
83 | $this->references[$label] = [
84 | 'url' => $this->replaceEscape($matches[2]),
85 | ];
86 | if (isset($matches[3])) {
87 | $this->references[$label]['title'] = $matches[3];
88 | } else {
89 | // title may be on the next line
90 | if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
91 | $this->references[$label]['title'] = $matches[1];
92 | $current++;
93 | }
94 | }
95 | if (isset($matches[5])) {
96 | $this->references[$label]['attributes'] = $matches[5];
97 | }
98 | $current++;
99 | }
100 | return [false, --$current];
101 | }
102 |
103 | /**
104 | * Consume lines for a fenced code block
105 | */
106 | protected function consumeFencedCode($lines, $current)
107 | {
108 | // consume until ```
109 | $block = [
110 | 'code',
111 | ];
112 | $line = rtrim($lines[$current]);
113 | if (($pos = strrpos($line, '`')) === false) {
114 | $pos = strrpos($line, '~');
115 | }
116 | $fence = substr($line, 0, $pos + 1);
117 | $block['attributes'] = substr($line, $pos);
118 | $content = [];
119 | for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
120 | if (rtrim($line = $lines[$i]) !== $fence) {
121 | $content[] = $line;
122 | } else {
123 | break;
124 | }
125 | }
126 | $block['content'] = implode("\n", $content);
127 | return [$block, $i];
128 | }
129 |
130 | protected function renderCode($block)
131 | {
132 | $attributes = $this->renderAttributes($block);
133 | return ($this->codeAttributesOnPre ? "" : "")
134 | . htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8')
135 | . "
\n";
136 | }
137 |
138 | /**
139 | * Renders a headline
140 | */
141 | protected function renderHeadline($block)
142 | {
143 | foreach($block['content'] as $i => $element) {
144 | if ($element[0] === 'specialAttributes') {
145 | unset($block['content'][$i]);
146 | $block['attributes'] = $element[1];
147 | }
148 | }
149 | $tag = 'h' . $block['level'];
150 | $attributes = $this->renderAttributes($block);
151 | return "<$tag$attributes>" . rtrim($this->renderAbsy($block['content']), "# \t") . "$tag>\n";
152 | }
153 |
154 | protected function renderAttributes($block)
155 | {
156 | $html = [];
157 | if (isset($block['attributes'])) {
158 | $attributes = preg_split('/\s+/', $block['attributes'], -1, PREG_SPLIT_NO_EMPTY);
159 | foreach($attributes as $attribute) {
160 | if ($attribute[0] === '#') {
161 | $html['id'] = substr($attribute, 1);
162 | } else {
163 | $html['class'][] = substr($attribute, 1);
164 | }
165 | }
166 | }
167 | $result = '';
168 | foreach($html as $attr => $value) {
169 | if (is_array($value)) {
170 | $value = trim(implode(' ', $value));
171 | }
172 | if (!empty($value)) {
173 | $result .= " $attr=\"$value\"";
174 | }
175 | }
176 | return $result;
177 | }
178 |
179 |
180 | // inline parsing
181 |
182 |
183 | /**
184 | * @marker {
185 | */
186 | protected function parseSpecialAttributes($text)
187 | {
188 | if (preg_match("~$this->_specialAttributesRegex~", $text, $matches)) {
189 | return [['specialAttributes', $matches[1]], strlen($matches[0])];
190 | }
191 | return [['text', '{'], 1];
192 | }
193 |
194 | protected function renderSpecialAttributes($block)
195 | {
196 | return '{' . $block[1] . '}';
197 | }
198 |
199 | protected function parseInline($text)
200 | {
201 | $elements = parent::parseInline($text);
202 | // merge special attribute elements to links and images as they are not part of the final absy later
203 | $relatedElement = null;
204 | foreach($elements as $i => $element) {
205 | if ($element[0] === 'link' || $element[0] === 'image') {
206 | $relatedElement = $i;
207 | } elseif ($element[0] === 'specialAttributes') {
208 | if ($relatedElement !== null) {
209 | $elements[$relatedElement]['attributes'] = $element[1];
210 | unset($elements[$i]);
211 | }
212 | $relatedElement = null;
213 | } else {
214 | $relatedElement = null;
215 | }
216 | }
217 | return $elements;
218 | }
219 |
220 | protected function renderLink($block)
221 | {
222 | if (isset($block['refkey'])) {
223 | if (($ref = $this->lookupReference($block['refkey'])) !== false) {
224 | $block = array_merge($block, $ref);
225 | } else {
226 | return $block['orig'];
227 | }
228 | }
229 | $attributes = $this->renderAttributes($block);
230 | return '' . $this->renderAbsy($block['text']) . '';
233 | }
234 |
235 | protected function renderImage($block)
236 | {
237 | if (isset($block['refkey'])) {
238 | if (($ref = $this->lookupReference($block['refkey'])) !== false) {
239 | $block = array_merge($block, $ref);
240 | } else {
241 | return $block['orig'];
242 | }
243 | }
244 | $attributes = $this->renderAttributes($block);
245 | return '
html5 ? '>' : ' />');
249 | }
250 | }
--------------------------------------------------------------------------------
/cebe-markdown/Parser.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | abstract class Parser
17 | {
18 | /**
19 | * @var integer the maximum nesting level for language elements.
20 | */
21 | public $maximumNestingLevel = 32;
22 |
23 | /**
24 | * @var string the current context the parser is in.
25 | * TODO remove in favor of absy
26 | */
27 | protected $context = [];
28 | /**
29 | * @var array these are "escapeable" characters. When using one of these prefixed with a
30 | * backslash, the character will be outputted without the backslash and is not interpreted
31 | * as markdown.
32 | */
33 | protected $escapeCharacters = [
34 | '\\', // backslash
35 | ];
36 |
37 | private $_depth = 0;
38 |
39 |
40 | /**
41 | * Parses the given text considering the full language.
42 | *
43 | * This includes parsing block elements as well as inline elements.
44 | *
45 | * @param string $text the text to parse
46 | * @return string parsed markup
47 | */
48 | public function parse($text)
49 | {
50 | $this->prepare();
51 |
52 | if (ltrim($text) === '') {
53 | return '';
54 | }
55 |
56 | $text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
57 |
58 | $this->prepareMarkers($text);
59 |
60 | $absy = $this->parseBlocks(explode("\n", $text));
61 | $markup = $this->renderAbsy($absy);
62 |
63 | $this->cleanup();
64 | return $markup;
65 | }
66 |
67 | /**
68 | * Parses a paragraph without block elements (block elements are ignored).
69 | *
70 | * @param string $text the text to parse
71 | * @return string parsed markup
72 | */
73 | public function parseParagraph($text)
74 | {
75 | $this->prepare();
76 |
77 | if (ltrim($text) === '') {
78 | return '';
79 | }
80 |
81 | $text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
82 |
83 | $this->prepareMarkers($text);
84 |
85 | $absy = $this->parseInline($text);
86 | $markup = $this->renderAbsy($absy);
87 |
88 | $this->cleanup();
89 | return $markup;
90 | }
91 |
92 | /**
93 | * This method will be called before `parse()` and `parseParagraph()`.
94 | * You can override it to do some initialization work.
95 | */
96 | protected function prepare()
97 | {
98 | }
99 |
100 | /**
101 | * This method will be called after `parse()` and `parseParagraph()`.
102 | * You can override it to do cleanup.
103 | */
104 | protected function cleanup()
105 | {
106 | }
107 |
108 |
109 | // block parsing
110 |
111 | private $_blockTypes;
112 |
113 | /**
114 | * @return array a list of block element types available.
115 | */
116 | protected function blockTypes()
117 | {
118 | if ($this->_blockTypes === null) {
119 | // detect block types via "identify" functions
120 | $reflection = new \ReflectionClass($this);
121 | $this->_blockTypes = array_filter(array_map(function($method) {
122 | $name = $method->getName();
123 | return strncmp($name, 'identify', 8) === 0 ? strtolower(substr($name, 8)) : false;
124 | }, $reflection->getMethods(ReflectionMethod::IS_PROTECTED)));
125 |
126 | sort($this->_blockTypes);
127 | }
128 | return $this->_blockTypes;
129 | }
130 |
131 | /**
132 | * Given a set of lines and an index of a current line it uses the registed block types to
133 | * detect the type of this line.
134 | * @param array $lines
135 | * @param integer $current
136 | * @return string name of the block type in lower case
137 | */
138 | protected function detectLineType($lines, $current)
139 | {
140 | $line = $lines[$current];
141 | $blockTypes = $this->blockTypes();
142 | foreach($blockTypes as $blockType) {
143 | if ($this->{'identify' . $blockType}($line, $lines, $current)) {
144 | return $blockType;
145 | }
146 | }
147 | // consider the line a normal paragraph if no other block type matches
148 | return 'paragraph';
149 | }
150 |
151 | /**
152 | * Parse block elements by calling `detectLineType()` to identify them
153 | * and call consume function afterwards.
154 | */
155 | protected function parseBlocks($lines)
156 | {
157 | if ($this->_depth >= $this->maximumNestingLevel) {
158 | // maximum depth is reached, do not parse input
159 | return [['text', implode("\n", $lines)]];
160 | }
161 | $this->_depth++;
162 |
163 | $blocks = [];
164 |
165 | // convert lines to blocks
166 | for ($i = 0, $count = count($lines); $i < $count; $i++) {
167 | $line = $lines[$i];
168 | if ($line !== '' && rtrim($line) !== '') { // skip empty lines
169 | // identify a blocks beginning and parse the content
170 | list($block, $i) = $this->parseBlock($lines, $i);
171 | if ($block !== false) {
172 | $blocks[] = $block;
173 | }
174 | }
175 | }
176 |
177 | $this->_depth--;
178 |
179 | return $blocks;
180 | }
181 |
182 | /**
183 | * Parses the block at current line by identifying the block type and parsing the content
184 | * @param $lines
185 | * @param $current
186 | * @return array Array of two elements, the first element contains the block,
187 | * the second contains the next line index to be parsed.
188 | */
189 | protected function parseBlock($lines, $current)
190 | {
191 | // identify block type for this line
192 | $blockType = $this->detectLineType($lines, $current);
193 |
194 | // call consume method for the detected block type to consume further lines
195 | return $this->{'consume' . $blockType}($lines, $current);
196 | }
197 |
198 | protected function renderAbsy($blocks)
199 | {
200 | $output = '';
201 | foreach ($blocks as $block) {
202 | array_unshift($this->context, $block[0]);
203 | $output .= $this->{'render' . $block[0]}($block);
204 | array_shift($this->context);
205 | }
206 | return $output;
207 | }
208 |
209 | /**
210 | * Consume lines for a paragraph
211 | *
212 | * @param $lines
213 | * @param $current
214 | * @return array
215 | */
216 | protected function consumeParagraph($lines, $current)
217 | {
218 | // consume until newline
219 | $content = [];
220 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
221 | if (ltrim($lines[$i]) !== '') {
222 | $content[] = $lines[$i];
223 | } else {
224 | break;
225 | }
226 | }
227 | $block = [
228 | 'paragraph',
229 | 'content' => $this->parseInline(implode("\n", $content)),
230 | ];
231 | return [$block, --$i];
232 | }
233 |
234 | /**
235 | * Render a paragraph block
236 | *
237 | * @param $block
238 | * @return string
239 | */
240 | protected function renderParagraph($block)
241 | {
242 | return '' . $this->renderAbsy($block['content']) . "
\n";
243 | }
244 |
245 |
246 | // inline parsing
247 |
248 |
249 | /**
250 | * @var array the set of inline markers to use in different contexts.
251 | */
252 | private $_inlineMarkers = [];
253 |
254 | /**
255 | * Returns a map of inline markers to the corresponding parser methods.
256 | *
257 | * This array defines handler methods for inline markdown markers.
258 | * When a marker is found in the text, the handler method is called with the text
259 | * starting at the position of the marker.
260 | *
261 | * Note that markers starting with whitespace may slow down the parser,
262 | * you may want to use [[renderText]] to deal with them.
263 | *
264 | * You may override this method to define a set of markers and parsing methods.
265 | * The default implementation looks for protected methods starting with `parse` that
266 | * also have an `@marker` annotation in PHPDoc.
267 | *
268 | * @return array a map of markers to parser methods
269 | */
270 | protected function inlineMarkers()
271 | {
272 | $markers = [];
273 | // detect "parse" functions
274 | $reflection = new \ReflectionClass($this);
275 | foreach($reflection->getMethods(ReflectionMethod::IS_PROTECTED) as $method) {
276 | $methodName = $method->getName();
277 | if (strncmp($methodName, 'parse', 5) === 0) {
278 | preg_match_all('/@marker ([^\s]+)/', $method->getDocComment(), $matches);
279 | foreach($matches[1] as $match) {
280 | $markers[$match] = $methodName;
281 | }
282 | }
283 | }
284 | return $markers;
285 | }
286 |
287 | /**
288 | * Prepare markers that are used in the text to parse
289 | *
290 | * Add all markers that are present in markdown.
291 | * Check is done to avoid iterations in parseInline(), good for huge markdown files
292 | * @param string $text
293 | */
294 | protected function prepareMarkers($text)
295 | {
296 | $this->_inlineMarkers = [];
297 | foreach ($this->inlineMarkers() as $marker => $method) {
298 | if (strpos($text, $marker) !== false) {
299 | $m = $marker[0];
300 | // put the longest marker first
301 | if (isset($this->_inlineMarkers[$m])) {
302 | reset($this->_inlineMarkers[$m]);
303 | if (strlen($marker) > strlen(key($this->_inlineMarkers[$m]))) {
304 | $this->_inlineMarkers[$m] = array_merge([$marker => $method], $this->_inlineMarkers[$m]);
305 | continue;
306 | }
307 | }
308 | $this->_inlineMarkers[$m][$marker] = $method;
309 | }
310 | }
311 | }
312 |
313 | /**
314 | * Parses inline elements of the language.
315 | *
316 | * @param string $text the inline text to parse.
317 | * @return array
318 | */
319 | protected function parseInline($text)
320 | {
321 | if ($this->_depth >= $this->maximumNestingLevel) {
322 | // maximum depth is reached, do not parse input
323 | return [['text', $text]];
324 | }
325 | $this->_depth++;
326 |
327 | $markers = implode('', array_keys($this->_inlineMarkers));
328 |
329 | $paragraph = [];
330 |
331 | while (!empty($markers) && ($found = strpbrk($text, $markers)) !== false) {
332 |
333 | $pos = strpos($text, $found);
334 |
335 | // add the text up to next marker to the paragraph
336 | if ($pos !== 0) {
337 | $paragraph[] = ['text', substr($text, 0, $pos)];
338 | }
339 | $text = $found;
340 |
341 | $parsed = false;
342 | foreach ($this->_inlineMarkers[$text[0]] as $marker => $method) {
343 | if (strncmp($text, $marker, strlen($marker)) === 0) {
344 | // parse the marker
345 | array_unshift($this->context, $method);
346 | list($output, $offset) = $this->$method($text);
347 | array_shift($this->context);
348 |
349 | $paragraph[] = $output;
350 | $text = substr($text, $offset);
351 | $parsed = true;
352 | break;
353 | }
354 | }
355 | if (!$parsed) {
356 | $paragraph[] = ['text', substr($text, 0, 1)];
357 | $text = substr($text, 1);
358 | }
359 | }
360 |
361 | $paragraph[] = ['text', $text];
362 |
363 | $this->_depth--;
364 |
365 | return $paragraph;
366 | }
367 |
368 | /**
369 | * Parses escaped special characters.
370 | * @marker \
371 | */
372 | protected function parseEscape($text)
373 | {
374 | if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
375 | return [['text', $text[1]], 2];
376 | }
377 | return [['text', $text[0]], 1];
378 | }
379 |
380 | /**
381 | * This function renders plain text sections in the markdown text.
382 | * It can be used to work on normal text sections for example to highlight keywords or
383 | * do special escaping.
384 | */
385 | protected function renderText($block)
386 | {
387 | return $block[1];
388 | }
389 | }
390 |
--------------------------------------------------------------------------------
/cebe-markdown/block/CodeTrait.php:
--------------------------------------------------------------------------------
1 | = 4 or one tab is code
21 | return ($l = $line[0]) === ' ' && $line[1] === ' ' && $line[2] === ' ' && $line[3] === ' ' || $l === "\t";
22 | }
23 |
24 | /**
25 | * Consume lines for a code block element
26 | */
27 | protected function consumeCode($lines, $current)
28 | {
29 | // consume until newline
30 |
31 | $content = [];
32 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
33 | $line = $lines[$i];
34 |
35 | // a line is considered to belong to this code block as long as it is intended by 4 spaces or a tab
36 | if (isset($line[0]) && ($line[0] === "\t" || strncmp($line, ' ', 4) === 0)) {
37 | $line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
38 | $content[] = $line;
39 | // but also if it is empty and the next line is intended by 4 spaces or a tab
40 | } elseif (($line === '' || rtrim($line) === '') && isset($lines[$i + 1][0]) &&
41 | ($lines[$i + 1][0] === "\t" || strncmp($lines[$i + 1], ' ', 4) === 0)) {
42 | if ($line !== '') {
43 | $line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
44 | }
45 | $content[] = $line;
46 | } else {
47 | break;
48 | }
49 | }
50 |
51 | $block = [
52 | 'code',
53 | 'content' => implode("\n", $content),
54 | ];
55 | return [$block, --$i];
56 | }
57 |
58 | /**
59 | * Renders a code block
60 | */
61 | protected function renderCode($block)
62 | {
63 | $class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : '';
64 | return "" . htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . "
\n";
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/cebe-markdown/block/FencedCodeTrait.php:
--------------------------------------------------------------------------------
1 | implode("\n", $content),
48 | ];
49 | if (!empty($language)) {
50 | $block['language'] = $language;
51 | }
52 | return [$block, $i];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/cebe-markdown/block/HeadlineTrait.php:
--------------------------------------------------------------------------------
1 | $this->parseInline(trim($lines[$current], "# \t")),
45 | 'level' => $level,
46 | ];
47 | return [$block, $current];
48 | } else {
49 | // underlined headline
50 | $block = [
51 | 'headline',
52 | 'content' => $this->parseInline($lines[$current]),
53 | 'level' => $lines[$current + 1][0] === '=' ? 1 : 2,
54 | ];
55 | return [$block, $current + 1];
56 | }
57 | }
58 |
59 | /**
60 | * Renders a headline
61 | */
62 | protected function renderHeadline($block)
63 | {
64 | $tag = 'h' . $block['level'];
65 | return "<$tag>" . $this->renderAbsy($block['content']) . "$tag>\n";
66 | }
67 |
68 | abstract protected function parseInline($text);
69 | abstract protected function renderAbsy($absy);
70 | }
71 |
--------------------------------------------------------------------------------
/cebe-markdown/block/HtmlTrait.php:
--------------------------------------------------------------------------------
1 | ');
61 | $spacePos = strpos($lines[$current], ' ');
62 | if ($gtPos === false && $spacePos === false) {
63 | return false; // no html tag
64 | } elseif ($spacePos === false) {
65 | $tag = rtrim(substr($line, 1, $gtPos - 1), '/');
66 | } else {
67 | $tag = rtrim(substr($line, 1, min($gtPos, $spacePos) - 1), '/');
68 | }
69 |
70 | if (!ctype_alnum($tag) || in_array(strtolower($tag), $this->inlineHtmlElements)) {
71 | return false; // no html tag or inline html tag
72 | }
73 | return true;
74 | }
75 |
76 | /**
77 | * Consume lines for an HTML block
78 | */
79 | protected function consumeHtml($lines, $current)
80 | {
81 | $content = [];
82 | if (strncmp($lines[$current], '') !== false) {
87 | break;
88 | }
89 | }
90 | } else {
91 | $tag = rtrim(substr($lines[$current], 1, min(strpos($lines[$current], '>'), strpos($lines[$current] . ' ', ' ')) - 1), '/');
92 | $level = 0;
93 | if (in_array($tag, $this->selfClosingHtmlElements)) {
94 | $level--;
95 | }
96 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
97 | $line = $lines[$i];
98 | $content[] = $line;
99 | $level += substr_count($line, "<$tag") - substr_count($line, "$tag>") - substr_count($line, "/>");
100 | if ($level <= 0) {
101 | break;
102 | }
103 | }
104 | }
105 | $block = [
106 | 'html',
107 | 'content' => implode("\n", $content),
108 | ];
109 | return [$block, $i];
110 | }
111 |
112 | /**
113 | * Renders an HTML block
114 | */
115 | protected function renderHtml($block)
116 | {
117 | return $block['content'] . "\n";
118 | }
119 |
120 | /**
121 | * Parses an & or a html entity definition.
122 | * @marker &
123 | */
124 | protected function parseEntity($text)
125 | {
126 | // html entities e.g. © © ©
127 | if (preg_match('/^?[\w\d]+;/', $text, $matches)) {
128 | return [['inlineHtml', $matches[0]], strlen($matches[0])];
129 | } else {
130 | return [['text', '&'], 1];
131 | }
132 | }
133 |
134 | /**
135 | * renders a html entity.
136 | */
137 | protected function renderInlineHtml($block)
138 | {
139 | return $block[1];
140 | }
141 |
142 | /**
143 | * Parses inline HTML.
144 | * @marker <
145 | */
146 | protected function parseInlineHtml($text)
147 | {
148 | if (strpos($text, '>') !== false) {
149 | if (preg_match('~^?(\w+\d?)( .*?)?>~s', $text, $matches)) {
150 | // HTML tags
151 | return [['inlineHtml', $matches[0]], strlen($matches[0])];
152 | } elseif (preg_match('~^~s', $text, $matches)) {
153 | // HTML comments
154 | return [['inlineHtml', $matches[0]], strlen($matches[0])];
155 | }
156 | }
157 | return [['text', '<'], 1];
158 | }
159 |
160 | /**
161 | * Escapes `>` characters.
162 | * @marker >
163 | */
164 | protected function parseGt($text)
165 | {
166 | return [['text', '>'], 1];
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/cebe-markdown/block/ListTrait.php:
--------------------------------------------------------------------------------
1 | ) starts with 1.
19 | */
20 | public $keepListStartNumber = false;
21 |
22 | /**
23 | * identify a line as the beginning of an ordered list.
24 | */
25 | protected function identifyOl($line)
26 | {
27 | return (($l = $line[0]) > '0' && $l <= '9' || $l === ' ') && preg_match('/^ {0,3}\d+\.[ \t]/', $line);
28 | }
29 |
30 | /**
31 | * identify a line as the beginning of an unordered list.
32 | */
33 | protected function identifyUl($line)
34 | {
35 | $l = $line[0];
36 | return ($l === '-' || $l === '+' || $l === '*') && (isset($line[1]) && (($l1 = $line[1]) === ' ' || $l1 === "\t")) ||
37 | ($l === ' ' && preg_match('/^ {0,3}[\-\+\*][ \t]/', $line));
38 | }
39 |
40 | /**
41 | * Consume lines for an ordered list
42 | */
43 | protected function consumeOl($lines, $current)
44 | {
45 | // consume until newline
46 |
47 | $block = [
48 | 'list',
49 | 'list' => 'ol',
50 | 'attr' => [],
51 | 'items' => [],
52 | ];
53 | return $this->consumeList($lines, $current, $block, 'ol');
54 | }
55 |
56 | /**
57 | * Consume lines for an unordered list
58 | */
59 | protected function consumeUl($lines, $current)
60 | {
61 | // consume until newline
62 |
63 | $block = [
64 | 'list',
65 | 'list' => 'ul',
66 | 'items' => [],
67 | ];
68 | return $this->consumeList($lines, $current, $block, 'ul');
69 | }
70 |
71 | private function consumeList($lines, $current, $block, $type)
72 | {
73 | $item = 0;
74 | $indent = '';
75 | $len = 0;
76 | $lastLineEmpty = false;
77 | // track the indentation of list markers, if indented more than previous element
78 | // a list marker is considered to be long to a lower level
79 | $leadSpace = 3;
80 | $marker = $type === 'ul' ? ltrim($lines[$current])[0] : '';
81 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
82 | $line = $lines[$i];
83 | // match list marker on the beginning of the line
84 | $pattern = ($type == 'ol') ? '/^( {0,'.$leadSpace.'})(\d+)\.[ \t]+/' : '/^( {0,'.$leadSpace.'})\\'.$marker.'[ \t]+/';
85 | if (preg_match($pattern, $line, $matches)) {
86 | if (($len = substr_count($matches[0], "\t")) > 0) {
87 | $indent = str_repeat("\t", $len);
88 | $line = substr($line, strlen($matches[0]));
89 | } else {
90 | $len = strlen($matches[0]);
91 | $indent = str_repeat(' ', $len);
92 | $line = substr($line, $len);
93 | }
94 | if ($i === $current) {
95 | $leadSpace = strlen($matches[1]) + 1;
96 | }
97 |
98 | if ($type == 'ol' && $this->keepListStartNumber) {
99 | // attr `start` for ol
100 | if (!isset($block['attr']['start']) && isset($matches[2])) {
101 | $block['attr']['start'] = $matches[2];
102 | }
103 | }
104 |
105 | $block['items'][++$item][] = $line;
106 | $block['lazyItems'][$item] = $lastLineEmpty;
107 | $lastLineEmpty = false;
108 | } elseif (ltrim($line) === '') {
109 | // line is empty, may be a lazy list
110 | $lastLineEmpty = true;
111 |
112 | // two empty lines will end the list
113 | if (!isset($lines[$i + 1][0])) {
114 | break;
115 |
116 | // next item is the continuation of this list -> lazy list
117 | } elseif (preg_match($pattern, $lines[$i + 1])) {
118 | $block['items'][$item][] = $line;
119 | $block['lazyItems'][$item] = true;
120 |
121 | // next item is indented as much as this list -> lazy list if it is not a reference
122 | } elseif (strncmp($lines[$i + 1], $indent, $len) === 0 || !empty($lines[$i + 1]) && $lines[$i + 1][0] == "\t") {
123 | $block['items'][$item][] = $line;
124 | $nextLine = $lines[$i + 1][0] === "\t" ? substr($lines[$i + 1], 1) : substr($lines[$i + 1], $len);
125 | $block['lazyItems'][$item] = !method_exists($this, 'identifyReference') || !$this->identifyReference($nextLine);
126 |
127 | // everything else ends the list
128 | } else {
129 | break;
130 | }
131 | } else {
132 | if ($line[0] === "\t") {
133 | $line = substr($line, 1);
134 | } elseif (strncmp($line, $indent, $len) === 0) {
135 | $line = substr($line, $len);
136 | }
137 | $block['items'][$item][] = $line;
138 | $lastLineEmpty = false;
139 | }
140 | }
141 |
142 | foreach($block['items'] as $itemId => $itemLines) {
143 | $content = [];
144 | if (!$block['lazyItems'][$itemId]) {
145 | $firstPar = [];
146 | while (!empty($itemLines) && rtrim($itemLines[0]) !== '' && $this->detectLineType($itemLines, 0) === 'paragraph') {
147 | $firstPar[] = array_shift($itemLines);
148 | }
149 | $content = $this->parseInline(implode("\n", $firstPar));
150 | }
151 | if (!empty($itemLines)) {
152 | $content = array_merge($content, $this->parseBlocks($itemLines));
153 | }
154 | $block['items'][$itemId] = $content;
155 | }
156 |
157 | return [$block, $i];
158 | }
159 |
160 | /**
161 | * Renders a list
162 | */
163 | protected function renderList($block)
164 | {
165 | $type = $block['list'];
166 |
167 | if (!empty($block['attr'])) {
168 | $output = "<$type " . $this->generateHtmlAttributes($block['attr']) . ">\n";
169 | } else {
170 | $output = "<$type>\n";
171 | }
172 |
173 | foreach ($block['items'] as $item => $itemLines) {
174 | $output .= '' . $this->renderAbsy($itemLines). " \n";
175 | }
176 | return $output . "$type>\n";
177 | }
178 |
179 |
180 | /**
181 | * Return html attributes string from [attrName => attrValue] list
182 | * @param array $attributes the attribute name-value pairs.
183 | * @return string
184 | */
185 | private function generateHtmlAttributes($attributes)
186 | {
187 | foreach ($attributes as $name => $value) {
188 | $attributes[$name] = "$name=\"$value\"";
189 | }
190 | return implode(' ', $attributes);
191 | }
192 |
193 | abstract protected function parseBlocks($lines);
194 | abstract protected function parseInline($text);
195 | abstract protected function renderAbsy($absy);
196 | abstract protected function detectLineType($lines, $current);
197 | }
198 |
--------------------------------------------------------------------------------
/cebe-markdown/block/QuoteTrait.php:
--------------------------------------------------------------------------------
1 | ' && (!isset($line[1]) || ($l1 = $line[1]) === ' ' || $l1 === "\t");
21 | }
22 |
23 | /**
24 | * Consume lines for a blockquote element
25 | */
26 | protected function consumeQuote($lines, $current)
27 | {
28 | // consume until newline
29 | $content = [];
30 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
31 | $line = $lines[$i];
32 | if (ltrim($line) !== '') {
33 | if ($line[0] == '>' && !isset($line[1])) {
34 | $line = '';
35 | } elseif (strncmp($line, '> ', 2) === 0) {
36 | $line = substr($line, 2);
37 | }
38 | $content[] = $line;
39 | } else {
40 | break;
41 | }
42 | }
43 |
44 | $block = [
45 | 'quote',
46 | 'content' => $this->parseBlocks($content),
47 | 'simple' => true,
48 | ];
49 | return [$block, $i];
50 | }
51 |
52 |
53 | /**
54 | * Renders a blockquote
55 | */
56 | protected function renderQuote($block)
57 | {
58 | return '' . $this->renderAbsy($block['content']) . "
\n";
59 | }
60 |
61 | abstract protected function parseBlocks($lines);
62 | abstract protected function renderAbsy($absy);
63 | }
64 |
--------------------------------------------------------------------------------
/cebe-markdown/block/RuleTrait.php:
--------------------------------------------------------------------------------
1 | html5 ? "
\n" : "
\n";
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/cebe-markdown/block/TableTrait.php:
--------------------------------------------------------------------------------
1 | [],
38 | 'rows' => [],
39 | ];
40 | $beginsWithPipe = $lines[$current][0] === '|';
41 | for ($i = $current, $count = count($lines); $i < $count; $i++) {
42 | $line = rtrim($lines[$i]);
43 |
44 | // extract alignment from second line
45 | if ($i == $current+1) {
46 | $cols = explode('|', trim($line, ' |'));
47 | foreach($cols as $col) {
48 | $col = trim($col);
49 | if (empty($col)) {
50 | $block['cols'][] = '';
51 | continue;
52 | }
53 | $l = ($col[0] === ':');
54 | $r = (substr($col, -1, 1) === ':');
55 | if ($l && $r) {
56 | $block['cols'][] = 'center';
57 | } elseif ($l) {
58 | $block['cols'][] = 'left';
59 | } elseif ($r) {
60 | $block['cols'][] = 'right';
61 | } else {
62 | $block['cols'][] = '';
63 | }
64 | }
65 |
66 | continue;
67 | }
68 | if ($line === '' || $beginsWithPipe && $line[0] !== '|') {
69 | break;
70 | }
71 | if ($line[0] === '|') {
72 | $line = substr($line, 1);
73 | }
74 | if (substr($line, -1, 1) === '|' && (substr($line, -2, 2) !== '\\|' || substr($line, -3, 3) === '\\\\|')) {
75 | $line = substr($line, 0, -1);
76 | }
77 | $block['rows'][] = $line;
78 | }
79 |
80 | return [$block, --$i];
81 | }
82 |
83 | /**
84 | * render a table block
85 | */
86 | protected function renderTable($block)
87 | {
88 | $content = '';
89 | $this->_tableCellAlign = $block['cols'];
90 | $content .= "\n";
91 | $first = true;
92 | foreach($block['rows'] as $row) {
93 | $this->_tableCellTag = $first ? 'th' : 'td';
94 | $align = empty($this->_tableCellAlign[$this->_tableCellCount]) ? '' : ' align="' . $this->_tableCellAlign[$this->_tableCellCount++] . '"';
95 | $tds = "<$this->_tableCellTag$align>" . trim($this->renderAbsy($this->parseInline($row))) . "$this->_tableCellTag>"; // TODO move this to the consume step
96 | $content .= "$tds \n";
97 | if ($first) {
98 | $content .= "\n\n";
99 | }
100 | $first = false;
101 | $this->_tableCellCount = 0;
102 | }
103 | return "\n$content\n
\n";
104 | }
105 |
106 | /**
107 | * @marker |
108 | */
109 | protected function parseTd($markdown)
110 | {
111 | if (isset($this->context[1]) && $this->context[1] === 'table') {
112 | $align = empty($this->_tableCellAlign[$this->_tableCellCount]) ? '' : ' align="' . $this->_tableCellAlign[$this->_tableCellCount++] . '"';
113 | return [['text', "$this->_tableCellTag><$this->_tableCellTag$align>"], isset($markdown[1]) && $markdown[1] === ' ' ? 2 : 1]; // TODO make a absy node
114 | }
115 | return [['text', $markdown[0]], 1];
116 | }
117 |
118 | abstract protected function parseInline($text);
119 | abstract protected function renderAbsy($absy);
120 | }
121 |
--------------------------------------------------------------------------------
/cebe-markdown/inline/CodeTrait.php:
--------------------------------------------------------------------------------
1 | ' . htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . '';
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/cebe-markdown/inline/EmphStrongTrait.php:
--------------------------------------------------------------------------------
1 | parseInline($matches[1]),
36 | ],
37 | strlen($matches[0])
38 | ];
39 | }
40 | } else { // emph
41 | if ($marker == '*' && preg_match('/^[*]((?:[^*]|[*][*][^*]+?[*][*])+?)[*](?![*][^*])/s', $text, $matches) ||
42 | $marker == '_' && preg_match('/^_((?:[^_]|__[^_]*__)+?)_(?!_[^_])\b/us', $text, $matches)) {
43 | return [
44 | [
45 | 'emph',
46 | $this->parseInline($matches[1]),
47 | ],
48 | strlen($matches[0])
49 | ];
50 | }
51 | }
52 | return [['text', $text[0]], 1];
53 | }
54 |
55 | protected function renderStrong($block)
56 | {
57 | return '' . $this->renderAbsy($block[1]) . '';
58 | }
59 |
60 | protected function renderEmph($block)
61 | {
62 | return '' . $this->renderAbsy($block[1]) . '';
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/cebe-markdown/inline/LinkTrait.php:
--------------------------------------------------------------------------------
1 | references = [];
35 | * }
36 | * ```
37 | */
38 | trait LinkTrait
39 | {
40 | /**
41 | * @var array a list of defined references in this document.
42 | */
43 | protected $references = [];
44 |
45 | /**
46 | * Remove backslash from escaped characters
47 | * @param $text
48 | * @return string
49 | */
50 | protected function replaceEscape($text)
51 | {
52 | $strtr = [];
53 | foreach($this->escapeCharacters as $char) {
54 | $strtr["\\$char"] = $char;
55 | }
56 | return strtr($text, $strtr);
57 | }
58 |
59 | /**
60 | * Parses a link indicated by `[`.
61 | * @marker [
62 | */
63 | protected function parseLink($markdown)
64 | {
65 | if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
66 | list($text, $url, $title, $offset, $key) = $parts;
67 | return [
68 | [
69 | 'link',
70 | 'text' => $this->parseInline($text),
71 | 'url' => $url,
72 | 'title' => $title,
73 | 'refkey' => $key,
74 | 'orig' => substr($markdown, 0, $offset),
75 | ],
76 | $offset
77 | ];
78 | } else {
79 | // remove all starting [ markers to avoid next one to be parsed as link
80 | $result = '[';
81 | $i = 1;
82 | while (isset($markdown[$i]) && $markdown[$i] == '[') {
83 | $result .= '[';
84 | $i++;
85 | }
86 | return [['text', $result], $i];
87 | }
88 | }
89 |
90 | /**
91 | * Parses an image indicated by `![`.
92 | * @marker ![
93 | */
94 | protected function parseImage($markdown)
95 | {
96 | if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
97 | list($text, $url, $title, $offset, $key) = $parts;
98 |
99 | return [
100 | [
101 | 'image',
102 | 'text' => $text,
103 | 'url' => $url,
104 | 'title' => $title,
105 | 'refkey' => $key,
106 | 'orig' => substr($markdown, 0, $offset + 1),
107 | ],
108 | $offset + 1
109 | ];
110 | } else {
111 | // remove all starting [ markers to avoid next one to be parsed as link
112 | $result = '!';
113 | $i = 1;
114 | while (isset($markdown[$i]) && $markdown[$i] == '[') {
115 | $result .= '[';
116 | $i++;
117 | }
118 | return [['text', $result], $i];
119 | }
120 | }
121 |
122 | protected function parseLinkOrImage($markdown)
123 | {
124 | if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
125 | $text = $textMatches[1];
126 | $offset = strlen($textMatches[0]);
127 | $markdown = substr($markdown, $offset);
128 |
129 | $pattern = <<[^\s()]+)|(?R))*\)
132 | | # else match a link with title
133 | ^\(\s*(((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\s*\)
134 | )/x
135 | REGEXP;
136 | if (preg_match($pattern, $markdown, $refMatches)) {
137 | // inline link
138 | return [
139 | $text,
140 | isset($refMatches[2]) ? $this->replaceEscape($refMatches[2]) : '', // url
141 | empty($refMatches[5]) ? null: $refMatches[5], // title
142 | $offset + strlen($refMatches[0]), // offset
143 | null, // reference key
144 | ];
145 | } elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
146 | // reference style link
147 | if (empty($refMatches[2])) {
148 | $key = strtolower($text);
149 | } else {
150 | $key = strtolower($refMatches[2]);
151 | }
152 | return [
153 | $text,
154 | null, // url
155 | null, // title
156 | $offset + strlen($refMatches[0]), // offset
157 | $key,
158 | ];
159 | }
160 | }
161 | return false;
162 | }
163 |
164 | /**
165 | * Parses inline HTML.
166 | * @marker <
167 | */
168 | protected function parseLt($text)
169 | {
170 | if (strpos($text, '>') !== false) {
171 | if (!in_array('parseLink', $this->context)) { // do not allow links in links
172 | if (preg_match('/^<([^\s]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
173 | // email address
174 | return [
175 | ['email', $this->replaceEscape($matches[1])],
176 | strlen($matches[0])
177 | ];
178 | } elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
179 | // URL
180 | return [
181 | ['url', $this->replaceEscape($matches[1])],
182 | strlen($matches[0])
183 | ];
184 | }
185 | }
186 | // try inline HTML if it was neither a URL nor email if HtmlTrait is included.
187 | if (method_exists($this, 'parseInlineHtml')) {
188 | return $this->parseInlineHtml($text);
189 | }
190 | }
191 | return [['text', '<'], 1];
192 | }
193 |
194 | protected function renderEmail($block)
195 | {
196 | $email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
197 | return "$email";
198 | }
199 |
200 | protected function renderUrl($block)
201 | {
202 | $url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
203 | $text = htmlspecialchars(urldecode($block[1]), ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
204 | return "$text";
205 | }
206 |
207 | protected function lookupReference($key)
208 | {
209 | $normalizedKey = preg_replace('/\s+/', ' ', $key);
210 | if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
211 | return $this->references[$key];
212 | }
213 | return false;
214 | }
215 |
216 | protected function renderLink($block)
217 | {
218 | if (isset($block['refkey'])) {
219 | if (($ref = $this->lookupReference($block['refkey'])) !== false) {
220 | $block = array_merge($block, $ref);
221 | } else {
222 | return $block['orig'];
223 | }
224 | }
225 | return '' . $this->renderAbsy($block['text']) . '';
228 | }
229 |
230 | protected function renderImage($block)
231 | {
232 | if (isset($block['refkey'])) {
233 | if (($ref = $this->lookupReference($block['refkey'])) !== false) {
234 | $block = array_merge($block, $ref);
235 | } else {
236 | return $block['orig'];
237 | }
238 | }
239 | return '
html5 ? '>' : ' />');
243 | }
244 |
245 | // references
246 |
247 | protected function identifyReference($line)
248 | {
249 | return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
250 | }
251 |
252 | /**
253 | * Consume link references
254 | */
255 | protected function consumeReference($lines, $current)
256 | {
257 | while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
258 | $label = strtolower($matches[1]);
259 |
260 | $this->references[$label] = [
261 | 'url' => $this->replaceEscape($matches[2]),
262 | ];
263 | if (isset($matches[3])) {
264 | $this->references[$label]['title'] = $matches[3];
265 | } else {
266 | // title may be on the next line
267 | if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
268 | $this->references[$label]['title'] = $matches[1];
269 | $current++;
270 | }
271 | }
272 | $current++;
273 | }
274 | return [false, --$current];
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/cebe-markdown/inline/StrikeoutTrait.php:
--------------------------------------------------------------------------------
1 | parseInline($matches[1])
26 | ],
27 | strlen($matches[0])
28 | ];
29 | }
30 | return [['text', $markdown[0] . $markdown[1]], 2];
31 | }
32 |
33 | protected function renderStrike($block)
34 | {
35 | return '' . $this->renderAbsy($block[1]) . '';
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/cebe-markdown/inline/UrlLinkTrait.php:
--------------------------------------------------------------------------------
1 | [^\s()]+)|(?R))*\)
28 | | # else match a link with title
29 | ^(https?|ftp):\/\/(([^\s()]+)|(?R))+(?context) && preg_match($pattern, $markdown, $matches)) {
34 | return [
35 | ['autoUrl', $matches[0]],
36 | strlen($matches[0])
37 | ];
38 | }
39 | return [['text', substr($markdown, 0, 4)], 4];
40 | }
41 |
42 | protected function renderAutoUrl($block)
43 | {
44 | $href = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
45 | $text = htmlspecialchars(urldecode($block[1]), ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
46 | return "$text";
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Markdown Editor",
3 | "uri": "https://github.com/svivian/q2a-markdown-editor",
4 | "description": "Markdown editor plugin for simple text-based markup",
5 | "version": "2.6.2",
6 | "date": "2017-09-07",
7 | "author": "Scott Vivian",
8 | "author_uri": "http://codelair.com",
9 | "license": "GPLv3",
10 | "update_uri": "https://raw.githubusercontent.com/svivian/q2a-markdown-editor/master/metadata.json",
11 | "min_q2a": "1.6",
12 | "min_php": "5.4"
13 | }
14 |
--------------------------------------------------------------------------------
/pagedown/LICENSE.txt:
--------------------------------------------------------------------------------
1 | A javascript port of Markdown, as used on Stack Overflow
2 | and the rest of Stack Exchange network.
3 |
4 | Largely based on showdown.js by John Fraser (Attacklab).
5 |
6 | Original Markdown Copyright (c) 2004-2005 John Gruber
7 |
8 |
9 |
10 | Original Showdown code copyright (c) 2007 John Fraser
11 |
12 | Modifications and bugfixes (c) 2009 Dana Robinson
13 | Modifications and bugfixes (c) 2009-2014 Stack Exchange Inc.
14 |
15 | Permission is hereby granted, free of charge, to any person obtaining a copy
16 | of this software and associated documentation files (the "Software"), to deal
17 | in the Software without restriction, including without limitation the rights
18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 | copies of the Software, and to permit persons to whom the Software is
20 | furnished to do so, subject to the following conditions:
21 |
22 | The above copyright notice and this permission notice shall be included in
23 | all copies or substantial portions of the Software.
24 |
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31 | THE SOFTWARE.
32 |
33 |
--------------------------------------------------------------------------------
/pagedown/Markdown.Sanitizer.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var output, Converter;
3 | if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
4 | output = exports;
5 | Converter = require("./Markdown.Converter").Converter;
6 | } else {
7 | output = window.Markdown;
8 | Converter = output.Converter;
9 | }
10 |
11 | output.getSanitizingConverter = function () {
12 | var converter = new Converter();
13 | converter.hooks.chain("postConversion", sanitizeHtml);
14 | converter.hooks.chain("postConversion", balanceTags);
15 | return converter;
16 | }
17 |
18 | function sanitizeHtml(html) {
19 | return html.replace(/<[^>]*>?/gi, sanitizeTag);
20 | }
21 |
22 | // (tags that can be opened/closed) | (tags that stand alone)
23 | var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol(?: start="\d+")?|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
24 | // |
25 | var a_white = /^(]+")?\s?>|<\/a>)$/i;
26 |
27 | //
]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
29 |
30 | function sanitizeTag(tag) {
31 | if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
32 | return tag;
33 | else
34 | return "";
35 | }
36 |
37 | ///
38 | /// attempt to balance HTML tags in the html string
39 | /// by removing any unmatched opening or closing tags
40 | /// IMPORTANT: we *assume* HTML has *already* been
41 | /// sanitized and is safe/sane before balancing!
42 | ///
43 | /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
44 | ///
45 | function balanceTags(html) {
46 |
47 | if (html == "")
48 | return "";
49 |
50 | var re = /<\/?\w+[^>]*(\s|$|>)/g;
51 | // convert everything to lower case; this makes
52 | // our case insensitive comparisons easier
53 | var tags = html.toLowerCase().match(re);
54 |
55 | // no HTML tags present? nothing to do; exit now
56 | var tagcount = (tags || []).length;
57 | if (tagcount == 0)
58 | return html;
59 |
60 | var tagname, tag;
61 | var ignoredtags = "![]()
";
62 | var match;
63 | var tagpaired = [];
64 | var tagremove = [];
65 | var needsRemoval = false;
66 |
67 | // loop through matched tags in forward order
68 | for (var ctag = 0; ctag < tagcount; ctag++) {
69 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
70 | // skip any already paired tags
71 | // and skip tags in our ignore list; assume they're self-closed
72 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
73 | continue;
74 |
75 | tag = tags[ctag];
76 | match = -1;
77 |
78 | if (!/^<\//.test(tag)) {
79 | // this is an opening tag
80 | // search forwards (next tags), look for closing tags
81 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
82 | if (!tagpaired[ntag] && tags[ntag] == "" + tagname + ">") {
83 | match = ntag;
84 | break;
85 | }
86 | }
87 | }
88 |
89 | if (match == -1)
90 | needsRemoval = tagremove[ctag] = true; // mark for removal
91 | else
92 | tagpaired[match] = true; // mark paired
93 | }
94 |
95 | if (!needsRemoval)
96 | return html;
97 |
98 | // delete all orphaned tags from the string
99 |
100 | var ctag = 0;
101 | html = html.replace(re, function (match) {
102 | var res = tagremove[ctag] ? "" : match;
103 | ctag++;
104 | return res;
105 | });
106 | return html;
107 | }
108 | })();
109 |
--------------------------------------------------------------------------------
/pagedown/highlight.min.js:
--------------------------------------------------------------------------------
1 | var hljs=new function(){function m(p){return p.replace(/&/gm,"&").replace(/"}while(y.length||w.length){var v=u().splice(0,1)[0];z+=m(x.substr(q,v.offset-q));q=v.offset;if(v.event=="start"){z+=t(v.node);s.push(v.node)}else{if(v.event=="stop"){var p,r=s.length;do{r--;p=s[r];z+=(""+p.nodeName.toLowerCase()+">")}while(p!=v.node);s.splice(r,1);while(r'+N[0]+""}else{r+=N[0]}P=Q.lR.lastIndex;N=Q.lR.exec(M)}return r+M.substr(P)}function B(M,N){var r;if(N.sL==""){r=g(M)}else{r=d(N.sL,M)}if(N.r>0){y+=r.keyword_count;C+=r.r}return''+r.value+""}function K(r,M){if(M.sL&&e[M.sL]||M.sL==""){return B(r,M)}else{return G(r,M)}}function J(N,r){var M=N.cN?'':"";if(N.rB){z+=M;N.buffer=""}else{if(N.eB){z+=m(r)+M;N.buffer=""}else{z+=M;N.buffer=r}}p.push(N);C+=N.r}function H(O,N,R){var S=p[p.length-1];if(R){z+=K(S.buffer+O,S);return false}var Q=s(N,S);if(Q){z+=K(S.buffer+O,S);J(Q,N);return Q.rB}var M=w(p.length-1,N);if(M){var P=S.cN?"":"";if(S.rE){z+=K(S.buffer+O,S)+P}else{if(S.eE){z+=K(S.buffer+O,S)+P+m(N)}else{z+=K(S.buffer+O+N,S)+P}}while(M>1){P=p[p.length-2].cN?"":"";z+=P;M--;p.length--}var r=p[p.length-1];p.length--;p[p.length-1].buffer="";if(r.starts){J(r.starts,"")}return S.rE}if(x(N,S)){throw"Illegal"}}var F=e[D];var p=[F.dM];var C=0;var y=0;var z="";try{var t,v=0;F.dM.buffer="";do{t=q(E,v);var u=H(t[0],t[1],t[2]);v+=t[0].length;if(!u){v+=t[1].length}}while(!t[2]);return{r:C,keyword_count:y,value:z,language:D}}catch(I){if(I=="Illegal"){return{r:0,keyword_count:0,value:m(E)}}else{throw I}}}function g(t){var p={keyword_count:0,r:0,value:m(t)};var r=p;for(var q in e){if(!e.hasOwnProperty(q)){continue}var s=d(q,t);s.language=q;if(s.keyword_count+s.r>r.keyword_count+r.r){r=s}if(s.keyword_count+s.r>p.keyword_count+p.r){r=p;p=s}}if(r.language){p.second_best=r}return p}function i(r,q,p){if(q){r=r.replace(/^((<[^>]+>|\t)+)/gm,function(t,w,v,u){return w.replace(/\t/g,q)})}if(p){r=r.replace(/\n/g,"
")}return r}function n(t,w,r){var x=h(t,r);var v=a(t);var y,s;if(v=="no-highlight"){return}if(v){y=d(v,x)}else{y=g(x);v=y.language}var q=c(t);if(q.length){s=document.createElement("pre");s.innerHTML=y.value;y.value=k(q,c(s),x)}y.value=i(y.value,w,r);var u=t.className;if(!u.match("(\\s|^)(language-)?"+v+"(\\s|$)")){u=u?(u+" "+v):v}if(/MSIE [678]/.test(navigator.userAgent)&&t.tagName=="CODE"&&t.parentNode.tagName=="PRE"){s=t.parentNode;var p=document.createElement("div");p.innerHTML=""+y.value+"
";t=p.firstChild.firstChild;p.firstChild.cN=s.cN;s.parentNode.replaceChild(p.firstChild,s)}else{t.innerHTML=y.value}t.className=u;t.result={language:v,kw:y.keyword_count,re:y.r};if(y.second_best){t.second_best={language:y.second_best.language,kw:y.second_best.keyword_count,re:y.second_best.r}}}function o(){if(o.called){return}o.called=true;var r=document.getElementsByTagName("pre");for(var p=0;p|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\.",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.inherit=function(r,s){var p={};for(var q in r){p[q]=r[q]}if(s){for(var q in s){p[q]=s[q]}}return p}}();hljs.LANGUAGES.bash=function(a){var f="true false";var c={cN:"variable",b:"\\$([a-zA-Z0-9_]+)\\b"};var b={cN:"variable",b:"\\$\\{(([^}])|(\\\\}))+\\}",c:[a.CNM]};var g={cN:"string",b:'"',e:'"',i:"\\n",c:[a.BE,c,b],r:0};var d={cN:"string",b:"'",e:"'",c:[{b:"''"}],r:0};var e={cN:"test_condition",b:"",e:"",c:[g,d,c,b,a.CNM],k:{literal:f},r:0};return{dM:{k:{keyword:"if then else fi for break continue while in do done echo exit return set declare",literal:f},c:[{cN:"shebang",b:"(#!\\/bin\\/bash)|(#!\\/bin\\/sh)",r:10},c,b,a.HCM,a.CNM,g,d,a.inherit(e,{b:"\\[ ",e:" \\]",r:0}),a.inherit(e,{b:"\\[\\[ ",e:" \\]\\]"})]}}}(hljs);hljs.LANGUAGES.cs=function(a){return{dM:{k:"abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual volatile void while ascending descending from get group into join let orderby partial select set value var where yield",c:[{cN:"comment",b:"///",e:"$",rB:true,c:[{cN:"xmlDocTag",b:"///|"},{cN:"xmlDocTag",b:"?",e:">"}]},a.CLCM,a.CBLCLM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line region endregion pragma checksum"},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},a.ASM,a.QSM,a.CNM]}}}(hljs);hljs.LANGUAGES.ruby=function(e){var a="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?";var k="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var g={keyword:"and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or def",keymethods:"__id__ __send__ abort abs all? allocate ancestors any? arity assoc at at_exit autoload autoload? between? binding binmode block_given? call callcc caller capitalize capitalize! casecmp catch ceil center chomp chomp! chop chop! chr class class_eval class_variable_defined? class_variables clear clone close close_read close_write closed? coerce collect collect! compact compact! concat const_defined? const_get const_missing const_set constants count crypt default default_proc delete delete! delete_at delete_if detect display div divmod downcase downcase! downto dump dup each each_byte each_index each_key each_line each_pair each_value each_with_index empty? entries eof eof? eql? equal? eval exec exit exit! extend fail fcntl fetch fileno fill find find_all first flatten flatten! floor flush for_fd foreach fork format freeze frozen? fsync getc gets global_variables grep gsub gsub! has_key? has_value? hash hex id include include? included_modules index indexes indices induced_from inject insert inspect instance_eval instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables integer? intern invert ioctl is_a? isatty iterator? join key? keys kind_of? lambda last length lineno ljust load local_variables loop lstrip lstrip! map map! match max member? merge merge! method method_defined? method_missing methods min module_eval modulo name nesting new next next! nil? nitems nonzero? object_id oct open pack partition pid pipe pop popen pos prec prec_f prec_i print printf private_class_method private_instance_methods private_method_defined? private_methods proc protected_instance_methods protected_method_defined? protected_methods public_class_method public_instance_methods public_method_defined? public_methods push putc puts quo raise rand rassoc read read_nonblock readchar readline readlines readpartial rehash reject reject! remainder reopen replace require respond_to? reverse reverse! reverse_each rewind rindex rjust round rstrip rstrip! scan seek select send set_trace_func shift singleton_method_added singleton_methods size sleep slice slice! sort sort! sort_by split sprintf squeeze squeeze! srand stat step store strip strip! sub sub! succ succ! sum superclass swapcase swapcase! sync syscall sysopen sysread sysseek system syswrite taint tainted? tell test throw times to_a to_ary to_f to_hash to_i to_int to_io to_proc to_s to_str to_sym tr tr! tr_s tr_s! trace_var transpose trap truncate tty? type ungetc uniq uniq! unpack unshift untaint untrace_var upcase upcase! update upto value? values values_at warn write write_nonblock zero? zip"};var c={cN:"yardoctag",b:"@[A-Za-z]+"};var l=[{cN:"comment",b:"#",e:"$",c:[c]},{cN:"comment",b:"^\\=begin",e:"^\\=end",c:[c],r:10},{cN:"comment",b:"^__END__",e:"\\n$"}];var d={cN:"subst",b:"#\\{",e:"}",l:a,k:g};var j=[e.BE,d];var b=[{cN:"string",b:"'",e:"'",c:j,r:0},{cN:"string",b:'"',e:'"',c:j,r:0},{cN:"string",b:"%[qw]?\\(",e:"\\)",c:j},{cN:"string",b:"%[qw]?\\[",e:"\\]",c:j},{cN:"string",b:"%[qw]?{",e:"}",c:j},{cN:"string",b:"%[qw]?<",e:">",c:j,r:10},{cN:"string",b:"%[qw]?/",e:"/",c:j,r:10},{cN:"string",b:"%[qw]?%",e:"%",c:j,r:10},{cN:"string",b:"%[qw]?-",e:"-",c:j,r:10},{cN:"string",b:"%[qw]?\\|",e:"\\|",c:j,r:10}];var i={cN:"function",b:"\\bdef\\s+",e:" |$|;",l:a,k:g,c:[{cN:"title",b:k,l:a,k:g},{cN:"params",b:"\\(",e:"\\)",l:a,k:g}].concat(l)};var h={cN:"identifier",b:a,l:a,k:g,r:0};var f=l.concat(b.concat([{cN:"class",bWK:true,e:"$|;",k:"class module",c:[{cN:"title",b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?",r:0},{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+e.IR+"::)?"+e.IR}]}].concat(l)},i,{cN:"constant",b:"(::)?([A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:b.concat([h]),r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"number",b:"\\?\\w"},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},h,{b:"("+e.RSR+")\\s*",c:l.concat([{cN:"regexp",b:"/",e:"/[a-z]*",i:"\\n",c:[e.BE]}]),r:0}]));d.c=f;i.c[1].c=f;return{dM:{l:a,k:g,c:f}}}(hljs);hljs.LANGUAGES.diff=function(a){return{cI:true,dM:{c:[{cN:"chunk",b:"^\\@\\@ +\\-\\d+,\\d+ +\\+\\d+,\\d+ +\\@\\@$",r:10},{cN:"chunk",b:"^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$",r:10},{cN:"chunk",b:"^\\-\\-\\- +\\d+,\\d+ +\\-\\-\\-\\-$",r:10},{cN:"header",b:"Index: ",e:"$"},{cN:"header",b:"=====",e:"=====$"},{cN:"header",b:"^\\-\\-\\-",e:"$"},{cN:"header",b:"^\\*{3} ",e:"$"},{cN:"header",b:"^\\+\\+\\+",e:"$"},{cN:"header",b:"\\*{5}",e:"\\*{5}$"},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}}}(hljs);hljs.LANGUAGES.javascript=function(a){return{dM:{k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete",literal:"true false null undefined NaN Infinity"},c:[a.ASM,a.QSM,a.CLCM,a.CBLCLM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBLCLM,{cN:"regexp",b:"/",e:"/[gim]*",c:[{b:"\\\\/"}]}],r:0},{cN:"function",bWK:true,e:"{",k:"function",c:[{cN:"title",b:"[A-Za-z$_][0-9A-Za-z$_]*"},{cN:"params",b:"\\(",e:"\\)",c:[a.CLCM,a.CBLCLM],i:"[\"'\\(]"}],i:"\\[|%"}]}}}(hljs);hljs.LANGUAGES.css=function(a){var b={cN:"function",b:a.IR+"\\(",e:"\\)",c:[{eW:true,eE:true,c:[a.NM,a.ASM,a.QSM]}]};return{cI:true,dM:{i:"[=/|']",c:[a.CBLCLM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",eE:true,k:"import page media charset",c:[b,a.ASM,a.QSM,a.NM]},{cN:"tag",b:a.IR,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[a.CBLCLM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[b,a.NM,a.QSM,a.ASM,a.CBLCLM,{cN:"hexcolor",b:"\\#[0-9A-F]+"},{cN:"important",b:"!important"}]}}]}]}]}}}(hljs);hljs.LANGUAGES.xml=function(a){var c="[A-Za-z0-9\\._:-]+";var b={eW:true,c:[{cN:"attribute",b:c,r:0},{b:'="',rB:true,e:'"',c:[{cN:"value",b:'"',eW:true}]},{b:"='",rB:true,e:"'",c:[{cN:"value",b:"'",eW:true}]},{b:"=",c:[{cN:"value",b:"[^\\s/>]+"}]}]};return{cI:true,dM:{c:[{cN:"pi",b:"<\\?",e:"\\?>",r:10},{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"",rE:true,sL:"css"}},{cN:"tag",b:"";
49 |
50 | // $html .= '' . "\n";
51 | // $html .= '' . "\n";
52 | // $html .= '' . "\n";
53 |
54 | // comment this script and uncomment the 3 above to use the non-minified code
55 | $html .= '' . "\n";
56 |
57 | return array('type'=>'custom', 'html'=>$html);
58 | }
59 |
60 | public function read_post($fieldname)
61 | {
62 | $html = $this->_my_qa_post_text($fieldname);
63 |
64 | return array(
65 | 'format' => 'markdown',
66 | 'content' => $html
67 | );
68 | }
69 |
70 | public function load_script($fieldname)
71 | {
72 | return
73 | 'var converter = Markdown.getSanitizingConverter();' . "\n" .
74 | 'var editor = new Markdown.Editor(converter, "-'.$fieldname.'");' . "\n" .
75 | 'editor.run();' . "\n";
76 | }
77 |
78 |
79 | // set admin options
80 | public function admin_form(&$qa_content)
81 | {
82 | $saved_msg = null;
83 |
84 | if (qa_clicked('markdown_save')) {
85 | // save options
86 | $hidecss = qa_post_text('md_hidecss') ? '1' : '0';
87 | qa_opt($this->cssopt, $hidecss);
88 | $convert = qa_post_text('md_comments') ? '1' : '0';
89 | qa_opt($this->convopt, $convert);
90 | $convert = qa_post_text('md_highlightjs') ? '1' : '0';
91 | qa_opt($this->hljsopt, $convert);
92 | $convert = qa_post_text('md_uploadimage') ? '1' : '0';
93 | qa_opt($this->impuplopt, $convert);
94 |
95 | $saved_msg = qa_lang_html('admin/options_saved');
96 | }
97 |
98 |
99 | return array(
100 | 'ok' => $saved_msg,
101 | 'style' => 'wide',
102 |
103 | 'fields' => array(
104 | 'css' => array(
105 | 'type' => 'checkbox',
106 | 'label' => qa_lang_html('markdown/admin_hidecss'),
107 | 'tags' => 'NAME="md_hidecss"',
108 | 'value' => qa_opt($this->cssopt) === '1',
109 | 'note' => qa_lang_html('markdown/admin_hidecss_note'),
110 | ),
111 | 'comments' => array(
112 | 'type' => 'checkbox',
113 | 'label' => qa_lang_html('markdown/admin_comments'),
114 | 'tags' => 'NAME="md_comments"',
115 | 'value' => qa_opt($this->convopt) === '1',
116 | 'note' => qa_lang_html('markdown/admin_comments_note'),
117 | ),
118 | 'highlightjs' => array(
119 | 'type' => 'checkbox',
120 | 'label' => qa_lang_html('markdown/admin_syntax'),
121 | 'tags' => 'NAME="md_highlightjs"',
122 | 'value' => qa_opt($this->hljsopt) === '1',
123 | 'note' => qa_lang_html('markdown/admin_syntax_note'),
124 | ),
125 | 'uploadimage' => array(
126 | 'type' => 'checkbox',
127 | 'label' => qa_lang_html('markdown/admin_image'),
128 | 'tags' => 'NAME="md_uploadimage"',
129 | 'value' => qa_opt($this->impuplopt) === '1',
130 | 'note' => qa_lang_html('markdown/admin_image_note'),
131 | )
132 | ),
133 |
134 | 'buttons' => array(
135 | 'save' => array(
136 | 'tags' => 'NAME="markdown_save"',
137 | 'label' => qa_lang_html('admin/save_options_button'),
138 | 'value' => '1',
139 | ),
140 | ),
141 | );
142 | }
143 |
144 |
145 | // copy of qa-base.php > qa_post_text, with trim() function removed.
146 | private function _my_qa_post_text($field)
147 | {
148 | return isset($_POST[$field]) ? preg_replace('/\r\n?/', "\n", qa_gpc_to_string($_POST[$field])) : null;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/qa-markdown-upload.php:
--------------------------------------------------------------------------------
1 | imguplopt) === '1';
20 |
21 | if (!$uploadimg) {
22 | $message = qa_lang('users/no_permission');
23 |
24 | header('Content-type: application/json');
25 | echo json_encode([
26 | 'error'=>$message
27 | ]);
28 | } else {
29 | require_once QA_INCLUDE_DIR.'app/upload.php';
30 |
31 | $upload = qa_upload_file_one(
32 | qa_get_max_upload_size(), // do not restrict upload size
33 | true, // force upload to image only
34 | 500, // max width (px)
35 | 500 // max height (px)
36 | );
37 |
38 | $message = @$upload['error'];
39 | $url = @$upload['bloburl'];
40 | $id = @$upload['blobid'];
41 |
42 |
43 | header('Content-type: application/json');
44 | echo json_encode([
45 | 'error'=>$message,
46 | 'url'=>$url,
47 | 'id'=>$id,
48 | ]);
49 | }
50 | }
51 |
52 | return null;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/qa-markdown-viewer.php:
--------------------------------------------------------------------------------
1 | plugindir = $directory;
14 | }
15 |
16 | public function calc_quality($content, $format)
17 | {
18 | return $format == 'markdown' ? 1.0 : 0.8;
19 | }
20 |
21 | public function get_html($content, $format, $options)
22 | {
23 | if (isset($options['blockwordspreg'])) {
24 | require_once QA_INCLUDE_DIR.'util/string.php';
25 | $content = qa_block_words_replace($content, $options['blockwordspreg']);
26 | }
27 |
28 | // include customized Markdown parser
29 | require_once 'MyMarkdown.php';
30 | $md = new MyMarkdown();
31 | $html = $md->parse($content);
32 |
33 | return qa_sanitize_html($html, @$options['linksnewwindow']);
34 | }
35 |
36 | public function get_text($content, $format, $options)
37 | {
38 | $viewer = qa_load_module('viewer', '');
39 | $text = $viewer->get_text($content, 'html', array());
40 |
41 | return $text;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/qa-md-events.php:
--------------------------------------------------------------------------------
1 | directory = $directory;
18 | $this->urltoroot = $urltoroot;
19 | }
20 |
21 | public function process_event($event, $userid, $handle, $cookieid, $params)
22 | {
23 | // check we have the correct event and the option is set
24 | if ($event != 'a_to_c')
25 | return;
26 | if (!qa_opt($this->convopt))
27 | return;
28 |
29 | qa_post_set_content($params['postid'], null, null, '', null, null, null, qa_get_logged_in_userid());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/qa-md-lang-default.php:
--------------------------------------------------------------------------------
1 | 'Markdown',
9 | 'preview' => 'Preview',
10 |
11 | 'admin_hidecss' => 'Don\'t add CSS inline',
12 | 'admin_hidecss_note' => 'Tick if you added the CSS to your own stylesheet (more efficient).',
13 | 'admin_comments' => 'Plaintext comments',
14 | 'admin_comments_note' => 'Sets a post as plaintext when converting answers to comments.',
15 | 'admin_syntax' => 'Use syntax highlighting',
16 | 'admin_syntax_note' => 'Integrates highlight.js for code blocks.',
17 | 'admin_image' => 'Allow image upload',
18 | 'admin_image_note' => 'Allows images to be uploaded directly through the editor.'
19 | );
20 |
--------------------------------------------------------------------------------
/qa-md-layer.php:
--------------------------------------------------------------------------------
1 | template, $tmpl))
18 | return;
19 |
20 | $hidecss = qa_opt($this->cssopt) === '1';
21 | $usehljs = qa_opt($this->hljsopt) === '1';
22 | $wmd_buttons = QA_HTML_THEME_LAYER_URLTOROOT.'pagedown/wmd-buttons.png';
23 |
24 | $this->output_raw(
25 | "\n\n");
43 |
44 | // set up HighlightJS
45 | if ($usehljs) {
46 | $js = file_get_contents(QA_HTML_THEME_LAYER_DIRECTORY.'pagedown/highlightjs-run.js');
47 |
48 | $this->output_raw(
49 | '' .
50 | ''
51 | );
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/qa-plugin.php:
--------------------------------------------------------------------------------
1 |