aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--COPYING4
-rw-r--r--application/Config.php114
-rw-r--r--application/PluginManager.php51
-rw-r--r--application/Router.php12
-rw-r--r--application/Utils.php8
-rw-r--r--inc/plugin_admin.js67
-rw-r--r--inc/shaarli.css61
-rw-r--r--index.php52
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.meta1
-rw-r--r--plugins/archiveorg/archiveorg.meta1
-rw-r--r--plugins/demo_plugin/demo_plugin.meta1
-rw-r--r--plugins/markdown/Parsedown.php1528
-rw-r--r--plugins/markdown/README.md51
-rw-r--r--plugins/markdown/help.html5
-rw-r--r--plugins/markdown/markdown.css137
-rw-r--r--plugins/markdown/markdown.meta1
-rw-r--r--plugins/markdown/markdown.php158
-rw-r--r--plugins/playvideos/playvideos.meta1
-rw-r--r--plugins/qrcode/qrcode.meta1
-rw-r--r--plugins/readityourself/readityourself.meta2
-rw-r--r--plugins/readityourself/readityourself.php2
-rw-r--r--plugins/wallabag/wallabag.meta2
-rw-r--r--plugins/wallabag/wallabag.php2
-rw-r--r--shaarli_version.php2
-rw-r--r--tests/ConfigTest.php109
-rw-r--r--tests/PluginManagerTest.php19
-rw-r--r--tests/plugins/PluginMarkdownTest.php112
-rw-r--r--tests/plugins/test/test.meta2
-rw-r--r--tpl/404.html2
-rw-r--r--tpl/pluginsadmin.html131
-rw-r--r--tpl/tools.html17
31 files changed, 2636 insertions, 20 deletions
diff --git a/COPYING b/COPYING
index 22929463..28939100 100644
--- a/COPYING
+++ b/COPYING
@@ -72,6 +72,10 @@ Files: plugins/wallabag/wallabag.png
72License: MIT License (http://opensource.org/licenses/MIT) 72License: MIT License (http://opensource.org/licenses/MIT)
73Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag 73Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag
74 74
75Files: plugins/markdown/Parsedown.php
76License: MIT License (http://opensource.org/licenses/MIT)
77Copyright: (C) 2015 Emanuil Rusev - https://github.com/erusev/parsedown
78
75---------------------------------------------------- 79----------------------------------------------------
76ZLIB/LIBPNG LICENSE 80ZLIB/LIBPNG LICENSE
77 81
diff --git a/application/Config.php b/application/Config.php
index c71ef68c..9af5a535 100644
--- a/application/Config.php
+++ b/application/Config.php
@@ -74,6 +74,106 @@ function writeConfig($config, $isLoggedIn)
74} 74}
75 75
76/** 76/**
77 * Process plugin administration form data and save it in an array.
78 *
79 * @param array $formData Data sent by the plugin admin form.
80 *
81 * @return array New list of enabled plugin, ordered.
82 *
83 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
84 */
85function save_plugin_config($formData)
86{
87 // Make sure there are no duplicates in orders.
88 if (!validate_plugin_order($formData)) {
89 throw new PluginConfigOrderException();
90 }
91
92 $plugins = array();
93 $newEnabledPlugins = array();
94 foreach ($formData as $key => $data) {
95 if (startsWith($key, 'order')) {
96 continue;
97 }
98
99 // If there is no order, it means a disabled plugin has been enabled.
100 if (isset($formData['order_' . $key])) {
101 $plugins[(int) $formData['order_' . $key]] = $key;
102 }
103 else {
104 $newEnabledPlugins[] = $key;
105 }
106 }
107
108 // New enabled plugins will be added at the end of order.
109 $plugins = array_merge($plugins, $newEnabledPlugins);
110
111 // Sort plugins by order.
112 if (!ksort($plugins)) {
113 throw new PluginConfigOrderException();
114 }
115
116 $finalPlugins = array();
117 // Make plugins order continuous.
118 foreach ($plugins as $plugin) {
119 $finalPlugins[] = $plugin;
120 }
121
122 return $finalPlugins;
123}
124
125/**
126 * Validate plugin array submitted.
127 * Will fail if there is duplicate orders value.
128 *
129 * @param array $formData Data from submitted form.
130 *
131 * @return bool true if ok, false otherwise.
132 */
133function validate_plugin_order($formData)
134{
135 $orders = array();
136 foreach ($formData as $key => $value) {
137 // No duplicate order allowed.
138 if (in_array($value, $orders)) {
139 return false;
140 }
141
142 if (startsWith($key, 'order')) {
143 $orders[] = $value;
144 }
145 }
146
147 return true;
148}
149
150/**
151 * Affect plugin parameters values into plugins array.
152 *
153 * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
154 * @param mixed $config Plugins configuration.
155 *
156 * @return mixed Updated $plugins array.
157 */
158function load_plugin_parameter_values($plugins, $config)
159{
160 $out = $plugins;
161 foreach ($plugins as $name => $plugin) {
162 if (empty($plugin['parameters'])) {
163 continue;
164 }
165
166 foreach ($plugin['parameters'] as $key => $param) {
167 if (!empty($config[$key])) {
168 $out[$name]['parameters'][$key] = $config[$key];
169 }
170 }
171 }
172
173 return $out;
174}
175
176/**
77 * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore. 177 * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore.
78 * ==> if user is loggedIn, merge its content with config.php, then delete options.php. 178 * ==> if user is loggedIn, merge its content with config.php, then delete options.php.
79 * 179 *
@@ -132,3 +232,17 @@ class UnauthorizedConfigException extends Exception
132 $this->message = 'You are not authorized to alter config.'; 232 $this->message = 'You are not authorized to alter config.';
133 } 233 }
134} 234}
235
236/**
237 * Exception used if an error occur while saving plugin configuration.
238 */
239class PluginConfigOrderException extends Exception
240{
241 /**
242 * Construct exception.
243 */
244 public function __construct()
245 {
246 $this->message = 'An error occurred while trying to save plugins loading order.';
247 }
248}
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 803f11b4..787ac6a9 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -34,6 +34,12 @@ class PluginManager
34 public static $PLUGINS_PATH = 'plugins'; 34 public static $PLUGINS_PATH = 'plugins';
35 35
36 /** 36 /**
37 * Plugins meta files extension.
38 * @var string $META_EXT
39 */
40 public static $META_EXT = 'meta';
41
42 /**
37 * Private constructor: new instances not allowed. 43 * Private constructor: new instances not allowed.
38 */ 44 */
39 private function __construct() 45 private function __construct()
@@ -162,6 +168,51 @@ class PluginManager
162 { 168 {
163 return 'hook_' . $pluginName . '_' . $hook; 169 return 'hook_' . $pluginName . '_' . $hook;
164 } 170 }
171
172 /**
173 * Retrieve plugins metadata from *.meta (INI) files into an array.
174 * Metadata contains:
175 * - plugin description [description]
176 * - parameters split with ';' [parameters]
177 *
178 * Respects plugins order from settings.
179 *
180 * @return array plugins metadata.
181 */
182 public function getPluginsMeta()
183 {
184 $metaData = array();
185 $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
186
187 // Browse all plugin directories.
188 foreach ($dirs as $pluginDir) {
189 $plugin = basename($pluginDir);
190 $metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
191 if (!is_file($metaFile) || !is_readable($metaFile)) {
192 continue;
193 }
194
195 $metaData[$plugin] = parse_ini_file($metaFile);
196 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
197
198 // Read parameters and format them into an array.
199 if (isset($metaData[$plugin]['parameters'])) {
200 $params = explode(';', $metaData[$plugin]['parameters']);
201 } else {
202 $params = array();
203 }
204 $metaData[$plugin]['parameters'] = array();
205 foreach ($params as $param) {
206 if (empty($param)) {
207 continue;
208 }
209
210 $metaData[$plugin]['parameters'][$param] = '';
211 }
212 }
213
214 return $metaData;
215 }
165} 216}
166 217
167/** 218/**
diff --git a/application/Router.php b/application/Router.php
index 0c813847..6185f08e 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -35,6 +35,10 @@ class Router
35 35
36 public static $PAGE_LINKLIST = 'linklist'; 36 public static $PAGE_LINKLIST = 'linklist';
37 37
38 public static $PAGE_PLUGINSADMIN = 'pluginadmin';
39
40 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
41
38 /** 42 /**
39 * Reproducing renderPage() if hell, to avoid regression. 43 * Reproducing renderPage() if hell, to avoid regression.
40 * 44 *
@@ -112,6 +116,14 @@ class Router
112 return self::$PAGE_IMPORT; 116 return self::$PAGE_IMPORT;
113 } 117 }
114 118
119 if (startswith($query, 'do='. self::$PAGE_PLUGINSADMIN)) {
120 return self::$PAGE_PLUGINSADMIN;
121 }
122
123 if (startswith($query, 'do='. self::$PAGE_SAVE_PLUGINSADMIN)) {
124 return self::$PAGE_SAVE_PLUGINSADMIN;
125 }
126
115 return self::$PAGE_LINKLIST; 127 return self::$PAGE_LINKLIST;
116 } 128 }
117} \ No newline at end of file 129} \ No newline at end of file
diff --git a/application/Utils.php b/application/Utils.php
index 551db3e7..10d60698 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -62,14 +62,6 @@ function endsWith($haystack, $needle, $case=true)
62} 62}
63 63
64/** 64/**
65 * Same as nl2br(), but escapes < and >
66 */
67function nl2br_escaped($html)
68{
69 return str_replace('>', '&gt;', str_replace('<', '&lt;', nl2br($html)));
70}
71
72/**
73 * htmlspecialchars wrapper 65 * htmlspecialchars wrapper
74 */ 66 */
75function escape($str) 67function escape($str)
diff --git a/inc/plugin_admin.js b/inc/plugin_admin.js
new file mode 100644
index 00000000..134ffb33
--- /dev/null
+++ b/inc/plugin_admin.js
@@ -0,0 +1,67 @@
1/**
2 * Change the position counter of a row.
3 *
4 * @param elem Element Node to change.
5 * @param toPos int New position.
6 */
7function changePos(elem, toPos)
8{
9 var elemName = elem.getAttribute('data-line')
10
11 elem.setAttribute('data-order', toPos);
12 var hiddenInput = document.querySelector('[name="order_'+ elemName +'"]');
13 hiddenInput.setAttribute('value', toPos);
14}
15
16/**
17 * Move a row up or down.
18 *
19 * @param pos Element Node to move.
20 * @param move int Move: +1 (down) or -1 (up)
21 */
22function changeOrder(pos, move)
23{
24 var newpos = parseInt(pos) + move;
25 var line = document.querySelector('[data-order="'+ pos +'"]');
26 var changeline = document.querySelector('[data-order="'+ newpos +'"]');
27 var parent = changeline.parentNode;
28
29 changePos(line, newpos);
30 changePos(changeline, parseInt(pos));
31 var changeItem = move < 0 ? changeline : changeline.nextSibling;
32 parent.insertBefore(line, changeItem);
33}
34
35/**
36 * Move a row up in the table.
37 *
38 * @param pos int row counter.
39 *
40 * @returns false
41 */
42function orderUp(pos)
43{
44 if (pos == 0) {
45 return false;
46 }
47 changeOrder(pos, -1);
48 return false;
49}
50
51/**
52 * Move a row down in the table.
53 *
54 * @param pos int row counter.
55 *
56 * @returns false
57 */
58function orderDown(pos)
59{
60 var lastpos = document.querySelector('[data-order]:last-child').getAttribute('data-order');
61 if (pos == lastpos) {
62 return false;
63 }
64
65 changeOrder(pos, +1);
66 return false;
67}
diff --git a/inc/shaarli.css b/inc/shaarli.css
index 96e2cae1..8a7409b2 100644
--- a/inc/shaarli.css
+++ b/inc/shaarli.css
@@ -467,7 +467,6 @@ h1 {
467 margin-top: 0; 467 margin-top: 0;
468 margin-bottom: 12px; 468 margin-bottom: 12px;
469 font-weight: normal; 469 font-weight: normal;
470 max-height: 400px;
471 overflow: auto; 470 overflow: auto;
472} 471}
473 472
@@ -1103,6 +1102,66 @@ ul.errors {
1103 float: left; 1102 float: left;
1104} 1103}
1105 1104
1105#pluginsadmin {
1106 width: 80%;
1107 padding: 20px 0 0 20px;
1108}
1109
1110#pluginsadmin section {
1111 padding: 20px 0;
1112}
1113
1114#pluginsadmin .plugin_parameters {
1115 margin: 10px 0;
1116}
1117
1118#pluginsadmin h1 {
1119 font-style: normal;
1120}
1121
1122#pluginsadmin h2 {
1123 font-size: 1.4em;
1124 font-weight: bold;
1125}
1126
1127#pluginsadmin table {
1128 width: 100%;
1129}
1130
1131#pluginsadmin table, #pluginsadmin th, #pluginsadmin td {
1132 border-width: 1px 0;
1133 border-style: solid;
1134 border-color: #c0c0c0;
1135}
1136
1137#pluginsadmin table th {
1138 font-weight: bold;
1139 padding: 10px 0;
1140}
1141
1142#pluginsadmin table td {
1143 padding: 5px 0;
1144}
1145
1146#pluginsadmin input[type=submit] {
1147 margin: 10px 0;
1148}
1149
1150#pluginsadmin .plugin_parameter {
1151 padding: 5px 0;
1152 border-width: 1px 0;
1153 border-style: solid;
1154 border-color: #c0c0c0;
1155}
1156
1157#pluginsadmin .float_label {
1158 float: left;
1159 width: 20%;
1160}
1161
1162#pluginsadmin a {
1163 color: black;
1164}
1106/* 404 page */ 1165/* 404 page */
1107.error-container { 1166.error-container {
1108 1167
diff --git a/index.php b/index.php
index beba9c32..31dcbf0f 100644
--- a/index.php
+++ b/index.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2/** 2/**
3 * Shaarli v0.6.2 - Shaare your links... 3 * Shaarli v0.6.3 - Shaare your links...
4 * 4 *
5 * The personal, minimalist, super-fast, no-database Delicious clone. 5 * The personal, minimalist, super-fast, no-database Delicious clone.
6 * 6 *
@@ -119,7 +119,7 @@ $GLOBALS['config']['PUBSUBHUB_URL'] = '';
119/* 119/*
120 * PHP configuration 120 * PHP configuration
121 */ 121 */
122define('shaarli_version', '0.6.2'); 122define('shaarli_version', '0.6.3');
123 123
124// http://server.com/x/shaarli --> /shaarli/ 124// http://server.com/x/shaarli --> /shaarli/
125define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); 125define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
@@ -1770,6 +1770,54 @@ HTML;
1770 exit; 1770 exit;
1771 } 1771 }
1772 1772
1773 // Plugin administration page
1774 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
1775 $pluginMeta = $pluginManager->getPluginsMeta();
1776
1777 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1778 $enabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] !== false; });
1779 // Load parameters.
1780 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $GLOBALS['plugins']);
1781 uasort(
1782 $enabledPlugins,
1783 function($a, $b) { return $a['order'] - $b['order']; }
1784 );
1785 $disabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] === false; });
1786
1787 $PAGE->assign('enabledPlugins', $enabledPlugins);
1788 $PAGE->assign('disabledPlugins', $disabledPlugins);
1789 $PAGE->renderPage('pluginsadmin');
1790 exit;
1791 }
1792
1793 // Plugin administration form action
1794 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1795 try {
1796 if (isset($_POST['parameters_form'])) {
1797 unset($_POST['parameters_form']);
1798 foreach ($_POST as $param => $value) {
1799 $GLOBALS['plugins'][$param] = escape($value);
1800 }
1801 }
1802 else {
1803 $GLOBALS['config']['ENABLED_PLUGINS'] = save_plugin_config($_POST);
1804 }
1805 writeConfig($GLOBALS, isLoggedIn());
1806 }
1807 catch (Exception $e) {
1808 error_log(
1809 'ERROR while saving plugin configuration:.' . PHP_EOL .
1810 $e->getMessage()
1811 );
1812
1813 // TODO: do not handle exceptions/errors in JS.
1814 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=pluginsadmin\';</script>';
1815 exit;
1816 }
1817 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
1818 exit;
1819 }
1820
1773 // -------- Otherwise, simply display search form and links: 1821 // -------- Otherwise, simply display search form and links:
1774 showLinkList($PAGE, $LINKSDB); 1822 showLinkList($PAGE, $LINKSDB);
1775 exit; 1823 exit;
diff --git a/plugins/addlink_toolbar/addlink_toolbar.meta b/plugins/addlink_toolbar/addlink_toolbar.meta
new file mode 100644
index 00000000..2f0b5866
--- /dev/null
+++ b/plugins/addlink_toolbar/addlink_toolbar.meta
@@ -0,0 +1 @@
description="Adds the addlink input on the linklist page."
diff --git a/plugins/archiveorg/archiveorg.meta b/plugins/archiveorg/archiveorg.meta
new file mode 100644
index 00000000..8b5703e1
--- /dev/null
+++ b/plugins/archiveorg/archiveorg.meta
@@ -0,0 +1 @@
description="For each link, add an Archive.org icon."
diff --git a/plugins/demo_plugin/demo_plugin.meta b/plugins/demo_plugin/demo_plugin.meta
new file mode 100644
index 00000000..b063ecb7
--- /dev/null
+++ b/plugins/demo_plugin/demo_plugin.meta
@@ -0,0 +1 @@
description="A demo plugin covering all use cases for template designers and plugin developers."
diff --git a/plugins/markdown/Parsedown.php b/plugins/markdown/Parsedown.php
new file mode 100644
index 00000000..91e05dcc
--- /dev/null
+++ b/plugins/markdown/Parsedown.php
@@ -0,0 +1,1528 @@
1<?php
2
3#
4#
5# Parsedown
6# http://parsedown.org
7#
8# (c) Emanuil Rusev
9# http://erusev.com
10#
11# For the full license information, view the LICENSE file that was distributed
12# with this source code.
13#
14#
15
16class Parsedown
17{
18 # ~
19
20 const version = '1.6.0';
21
22 # ~
23
24 function text($text)
25 {
26 # make sure no definitions are set
27 $this->DefinitionData = array();
28
29 # standardize line breaks
30 $text = str_replace(array("\r\n", "\r"), "\n", $text);
31
32 # remove surrounding line breaks
33 $text = trim($text, "\n");
34
35 # split text into lines
36 $lines = explode("\n", $text);
37
38 # iterate through lines to identify blocks
39 $markup = $this->lines($lines);
40
41 # trim line breaks
42 $markup = trim($markup, "\n");
43
44 return $markup;
45 }
46
47 #
48 # Setters
49 #
50
51 function setBreaksEnabled($breaksEnabled)
52 {
53 $this->breaksEnabled = $breaksEnabled;
54
55 return $this;
56 }
57
58 protected $breaksEnabled;
59
60 function setMarkupEscaped($markupEscaped)
61 {
62 $this->markupEscaped = $markupEscaped;
63
64 return $this;
65 }
66
67 protected $markupEscaped;
68
69 function setUrlsLinked($urlsLinked)
70 {
71 $this->urlsLinked = $urlsLinked;
72
73 return $this;
74 }
75
76 protected $urlsLinked = true;
77
78 #
79 # Lines
80 #
81
82 protected $BlockTypes = array(
83 '#' => array('Header'),
84 '*' => array('Rule', 'List'),
85 '+' => array('List'),
86 '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
87 '0' => array('List'),
88 '1' => array('List'),
89 '2' => array('List'),
90 '3' => array('List'),
91 '4' => array('List'),
92 '5' => array('List'),
93 '6' => array('List'),
94 '7' => array('List'),
95 '8' => array('List'),
96 '9' => array('List'),
97 ':' => array('Table'),
98 '<' => array('Comment', 'Markup'),
99 '=' => array('SetextHeader'),
100 '>' => array('Quote'),
101 '[' => array('Reference'),
102 '_' => array('Rule'),
103 '`' => array('FencedCode'),
104 '|' => array('Table'),
105 '~' => array('FencedCode'),
106 );
107
108 # ~
109
110 protected $unmarkedBlockTypes = array(
111 'Code',
112 );
113
114 #
115 # Blocks
116 #
117
118 private function lines(array $lines)
119 {
120 $CurrentBlock = null;
121
122 foreach ($lines as $line)
123 {
124 if (chop($line) === '')
125 {
126 if (isset($CurrentBlock))
127 {
128 $CurrentBlock['interrupted'] = true;
129 }
130
131 continue;
132 }
133
134 if (strpos($line, "\t") !== false)
135 {
136 $parts = explode("\t", $line);
137
138 $line = $parts[0];
139
140 unset($parts[0]);
141
142 foreach ($parts as $part)
143 {
144 $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
145
146 $line .= str_repeat(' ', $shortage);
147 $line .= $part;
148 }
149 }
150
151 $indent = 0;
152
153 while (isset($line[$indent]) and $line[$indent] === ' ')
154 {
155 $indent ++;
156 }
157
158 $text = $indent > 0 ? substr($line, $indent) : $line;
159
160 # ~
161
162 $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
163
164 # ~
165
166 if (isset($CurrentBlock['continuable']))
167 {
168 $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
169
170 if (isset($Block))
171 {
172 $CurrentBlock = $Block;
173
174 continue;
175 }
176 else
177 {
178 if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
179 {
180 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
181 }
182 }
183 }
184
185 # ~
186
187 $marker = $text[0];
188
189 # ~
190
191 $blockTypes = $this->unmarkedBlockTypes;
192
193 if (isset($this->BlockTypes[$marker]))
194 {
195 foreach ($this->BlockTypes[$marker] as $blockType)
196 {
197 $blockTypes []= $blockType;
198 }
199 }
200
201 #
202 # ~
203
204 foreach ($blockTypes as $blockType)
205 {
206 $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
207
208 if (isset($Block))
209 {
210 $Block['type'] = $blockType;
211
212 if ( ! isset($Block['identified']))
213 {
214 $Blocks []= $CurrentBlock;
215
216 $Block['identified'] = true;
217 }
218
219 if (method_exists($this, 'block'.$blockType.'Continue'))
220 {
221 $Block['continuable'] = true;
222 }
223
224 $CurrentBlock = $Block;
225
226 continue 2;
227 }
228 }
229
230 # ~
231
232 if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
233 {
234 $CurrentBlock['element']['text'] .= "\n".$text;
235 }
236 else
237 {
238 $Blocks []= $CurrentBlock;
239
240 $CurrentBlock = $this->paragraph($Line);
241
242 $CurrentBlock['identified'] = true;
243 }
244 }
245
246 # ~
247
248 if (isset($CurrentBlock['continuable']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
249 {
250 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
251 }
252
253 # ~
254
255 $Blocks []= $CurrentBlock;
256
257 unset($Blocks[0]);
258
259 # ~
260
261 $markup = '';
262
263 foreach ($Blocks as $Block)
264 {
265 if (isset($Block['hidden']))
266 {
267 continue;
268 }
269
270 $markup .= "\n";
271 $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
272 }
273
274 $markup .= "\n";
275
276 # ~
277
278 return $markup;
279 }
280
281 #
282 # Code
283
284 protected function blockCode($Line, $Block = null)
285 {
286 if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
287 {
288 return;
289 }
290
291 if ($Line['indent'] >= 4)
292 {
293 $text = substr($Line['body'], 4);
294
295 $Block = array(
296 'element' => array(
297 'name' => 'pre',
298 'handler' => 'element',
299 'text' => array(
300 'name' => 'code',
301 'text' => $text,
302 ),
303 ),
304 );
305
306 return $Block;
307 }
308 }
309
310 protected function blockCodeContinue($Line, $Block)
311 {
312 if ($Line['indent'] >= 4)
313 {
314 if (isset($Block['interrupted']))
315 {
316 $Block['element']['text']['text'] .= "\n";
317
318 unset($Block['interrupted']);
319 }
320
321 $Block['element']['text']['text'] .= "\n";
322
323 $text = substr($Line['body'], 4);
324
325 $Block['element']['text']['text'] .= $text;
326
327 return $Block;
328 }
329 }
330
331 protected function blockCodeComplete($Block)
332 {
333 $text = $Block['element']['text']['text'];
334
335 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
336
337 $Block['element']['text']['text'] = $text;
338
339 return $Block;
340 }
341
342 #
343 # Comment
344
345 protected function blockComment($Line)
346 {
347 if ($this->markupEscaped)
348 {
349 return;
350 }
351
352 if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
353 {
354 $Block = array(
355 'markup' => $Line['body'],
356 );
357
358 if (preg_match('/-->$/', $Line['text']))
359 {
360 $Block['closed'] = true;
361 }
362
363 return $Block;
364 }
365 }
366
367 protected function blockCommentContinue($Line, array $Block)
368 {
369 if (isset($Block['closed']))
370 {
371 return;
372 }
373
374 $Block['markup'] .= "\n" . $Line['body'];
375
376 if (preg_match('/-->$/', $Line['text']))
377 {
378 $Block['closed'] = true;
379 }
380
381 return $Block;
382 }
383
384 #
385 # Fenced Code
386
387 protected function blockFencedCode($Line)
388 {
389 if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
390 {
391 $Element = array(
392 'name' => 'code',
393 'text' => '',
394 );
395
396 if (isset($matches[1]))
397 {
398 $class = 'language-'.$matches[1];
399
400 $Element['attributes'] = array(
401 'class' => $class,
402 );
403 }
404
405 $Block = array(
406 'char' => $Line['text'][0],
407 'element' => array(
408 'name' => 'pre',
409 'handler' => 'element',
410 'text' => $Element,
411 ),
412 );
413
414 return $Block;
415 }
416 }
417
418 protected function blockFencedCodeContinue($Line, $Block)
419 {
420 if (isset($Block['complete']))
421 {
422 return;
423 }
424
425 if (isset($Block['interrupted']))
426 {
427 $Block['element']['text']['text'] .= "\n";
428
429 unset($Block['interrupted']);
430 }
431
432 if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
433 {
434 $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
435
436 $Block['complete'] = true;
437
438 return $Block;
439 }
440
441 $Block['element']['text']['text'] .= "\n".$Line['body'];;
442
443 return $Block;
444 }
445
446 protected function blockFencedCodeComplete($Block)
447 {
448 $text = $Block['element']['text']['text'];
449
450 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
451
452 $Block['element']['text']['text'] = $text;
453
454 return $Block;
455 }
456
457 #
458 # Header
459
460 protected function blockHeader($Line)
461 {
462 if (isset($Line['text'][1]))
463 {
464 $level = 1;
465
466 while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
467 {
468 $level ++;
469 }
470
471 if ($level > 6)
472 {
473 return;
474 }
475
476 $text = trim($Line['text'], '# ');
477
478 $Block = array(
479 'element' => array(
480 'name' => 'h' . min(6, $level),
481 'text' => $text,
482 'handler' => 'line',
483 ),
484 );
485
486 return $Block;
487 }
488 }
489
490 #
491 # List
492
493 protected function blockList($Line)
494 {
495 list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
496
497 if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
498 {
499 $Block = array(
500 'indent' => $Line['indent'],
501 'pattern' => $pattern,
502 'element' => array(
503 'name' => $name,
504 'handler' => 'elements',
505 ),
506 );
507
508 $Block['li'] = array(
509 'name' => 'li',
510 'handler' => 'li',
511 'text' => array(
512 $matches[2],
513 ),
514 );
515
516 $Block['element']['text'] []= & $Block['li'];
517
518 return $Block;
519 }
520 }
521
522 protected function blockListContinue($Line, array $Block)
523 {
524 if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
525 {
526 if (isset($Block['interrupted']))
527 {
528 $Block['li']['text'] []= '';
529
530 unset($Block['interrupted']);
531 }
532
533 unset($Block['li']);
534
535 $text = isset($matches[1]) ? $matches[1] : '';
536
537 $Block['li'] = array(
538 'name' => 'li',
539 'handler' => 'li',
540 'text' => array(
541 $text,
542 ),
543 );
544
545 $Block['element']['text'] []= & $Block['li'];
546
547 return $Block;
548 }
549
550 if ($Line['text'][0] === '[' and $this->blockReference($Line))
551 {
552 return $Block;
553 }
554
555 if ( ! isset($Block['interrupted']))
556 {
557 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
558
559 $Block['li']['text'] []= $text;
560
561 return $Block;
562 }
563
564 if ($Line['indent'] > 0)
565 {
566 $Block['li']['text'] []= '';
567
568 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
569
570 $Block['li']['text'] []= $text;
571
572 unset($Block['interrupted']);
573
574 return $Block;
575 }
576 }
577
578 #
579 # Quote
580
581 protected function blockQuote($Line)
582 {
583 if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
584 {
585 $Block = array(
586 'element' => array(
587 'name' => 'blockquote',
588 'handler' => 'lines',
589 'text' => (array) $matches[1],
590 ),
591 );
592
593 return $Block;
594 }
595 }
596
597 protected function blockQuoteContinue($Line, array $Block)
598 {
599 if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
600 {
601 if (isset($Block['interrupted']))
602 {
603 $Block['element']['text'] []= '';
604
605 unset($Block['interrupted']);
606 }
607
608 $Block['element']['text'] []= $matches[1];
609
610 return $Block;
611 }
612
613 if ( ! isset($Block['interrupted']))
614 {
615 $Block['element']['text'] []= $Line['text'];
616
617 return $Block;
618 }
619 }
620
621 #
622 # Rule
623
624 protected function blockRule($Line)
625 {
626 if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
627 {
628 $Block = array(
629 'element' => array(
630 'name' => 'hr'
631 ),
632 );
633
634 return $Block;
635 }
636 }
637
638 #
639 # Setext
640
641 protected function blockSetextHeader($Line, array $Block = null)
642 {
643 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
644 {
645 return;
646 }
647
648 if (chop($Line['text'], $Line['text'][0]) === '')
649 {
650 $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
651
652 return $Block;
653 }
654 }
655
656 #
657 # Markup
658
659 protected function blockMarkup($Line)
660 {
661 if ($this->markupEscaped)
662 {
663 return;
664 }
665
666 if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
667 {
668 $element = strtolower($matches[1]);
669
670 if (in_array($element, $this->textLevelElements))
671 {
672 return;
673 }
674
675 $Block = array(
676 'name' => $matches[1],
677 'depth' => 0,
678 'markup' => $Line['text'],
679 );
680
681 $length = strlen($matches[0]);
682
683 $remainder = substr($Line['text'], $length);
684
685 if (trim($remainder) === '')
686 {
687 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
688 {
689 $Block['closed'] = true;
690
691 $Block['void'] = true;
692 }
693 }
694 else
695 {
696 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
697 {
698 return;
699 }
700
701 if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
702 {
703 $Block['closed'] = true;
704 }
705 }
706
707 return $Block;
708 }
709 }
710
711 protected function blockMarkupContinue($Line, array $Block)
712 {
713 if (isset($Block['closed']))
714 {
715 return;
716 }
717
718 if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
719 {
720 $Block['depth'] ++;
721 }
722
723 if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
724 {
725 if ($Block['depth'] > 0)
726 {
727 $Block['depth'] --;
728 }
729 else
730 {
731 $Block['closed'] = true;
732 }
733 }
734
735 if (isset($Block['interrupted']))
736 {
737 $Block['markup'] .= "\n";
738
739 unset($Block['interrupted']);
740 }
741
742 $Block['markup'] .= "\n".$Line['body'];
743
744 return $Block;
745 }
746
747 #
748 # Reference
749
750 protected function blockReference($Line)
751 {
752 if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
753 {
754 $id = strtolower($matches[1]);
755
756 $Data = array(
757 'url' => $matches[2],
758 'title' => null,
759 );
760
761 if (isset($matches[3]))
762 {
763 $Data['title'] = $matches[3];
764 }
765
766 $this->DefinitionData['Reference'][$id] = $Data;
767
768 $Block = array(
769 'hidden' => true,
770 );
771
772 return $Block;
773 }
774 }
775
776 #
777 # Table
778
779 protected function blockTable($Line, array $Block = null)
780 {
781 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
782 {
783 return;
784 }
785
786 if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
787 {
788 $alignments = array();
789
790 $divider = $Line['text'];
791
792 $divider = trim($divider);
793 $divider = trim($divider, '|');
794
795 $dividerCells = explode('|', $divider);
796
797 foreach ($dividerCells as $dividerCell)
798 {
799 $dividerCell = trim($dividerCell);
800
801 if ($dividerCell === '')
802 {
803 continue;
804 }
805
806 $alignment = null;
807
808 if ($dividerCell[0] === ':')
809 {
810 $alignment = 'left';
811 }
812
813 if (substr($dividerCell, - 1) === ':')
814 {
815 $alignment = $alignment === 'left' ? 'center' : 'right';
816 }
817
818 $alignments []= $alignment;
819 }
820
821 # ~
822
823 $HeaderElements = array();
824
825 $header = $Block['element']['text'];
826
827 $header = trim($header);
828 $header = trim($header, '|');
829
830 $headerCells = explode('|', $header);
831
832 foreach ($headerCells as $index => $headerCell)
833 {
834 $headerCell = trim($headerCell);
835
836 $HeaderElement = array(
837 'name' => 'th',
838 'text' => $headerCell,
839 'handler' => 'line',
840 );
841
842 if (isset($alignments[$index]))
843 {
844 $alignment = $alignments[$index];
845
846 $HeaderElement['attributes'] = array(
847 'style' => 'text-align: '.$alignment.';',
848 );
849 }
850
851 $HeaderElements []= $HeaderElement;
852 }
853
854 # ~
855
856 $Block = array(
857 'alignments' => $alignments,
858 'identified' => true,
859 'element' => array(
860 'name' => 'table',
861 'handler' => 'elements',
862 ),
863 );
864
865 $Block['element']['text'] []= array(
866 'name' => 'thead',
867 'handler' => 'elements',
868 );
869
870 $Block['element']['text'] []= array(
871 'name' => 'tbody',
872 'handler' => 'elements',
873 'text' => array(),
874 );
875
876 $Block['element']['text'][0]['text'] []= array(
877 'name' => 'tr',
878 'handler' => 'elements',
879 'text' => $HeaderElements,
880 );
881
882 return $Block;
883 }
884 }
885
886 protected function blockTableContinue($Line, array $Block)
887 {
888 if (isset($Block['interrupted']))
889 {
890 return;
891 }
892
893 if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
894 {
895 $Elements = array();
896
897 $row = $Line['text'];
898
899 $row = trim($row);
900 $row = trim($row, '|');
901
902 preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
903
904 foreach ($matches[0] as $index => $cell)
905 {
906 $cell = trim($cell);
907
908 $Element = array(
909 'name' => 'td',
910 'handler' => 'line',
911 'text' => $cell,
912 );
913
914 if (isset($Block['alignments'][$index]))
915 {
916 $Element['attributes'] = array(
917 'style' => 'text-align: '.$Block['alignments'][$index].';',
918 );
919 }
920
921 $Elements []= $Element;
922 }
923
924 $Element = array(
925 'name' => 'tr',
926 'handler' => 'elements',
927 'text' => $Elements,
928 );
929
930 $Block['element']['text'][1]['text'] []= $Element;
931
932 return $Block;
933 }
934 }
935
936 #
937 # ~
938 #
939
940 protected function paragraph($Line)
941 {
942 $Block = array(
943 'element' => array(
944 'name' => 'p',
945 'text' => $Line['text'],
946 'handler' => 'line',
947 ),
948 );
949
950 return $Block;
951 }
952
953 #
954 # Inline Elements
955 #
956
957 protected $InlineTypes = array(
958 '"' => array('SpecialCharacter'),
959 '!' => array('Image'),
960 '&' => array('SpecialCharacter'),
961 '*' => array('Emphasis'),
962 ':' => array('Url'),
963 '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
964 '>' => array('SpecialCharacter'),
965 '[' => array('Link'),
966 '_' => array('Emphasis'),
967 '`' => array('Code'),
968 '~' => array('Strikethrough'),
969 '\\' => array('EscapeSequence'),
970 );
971
972 # ~
973
974 protected $inlineMarkerList = '!"*_&[:<>`~\\';
975
976 #
977 # ~
978 #
979
980 public function line($text)
981 {
982 $markup = '';
983
984 # $excerpt is based on the first occurrence of a marker
985
986 while ($excerpt = strpbrk($text, $this->inlineMarkerList))
987 {
988 $marker = $excerpt[0];
989
990 $markerPosition = strpos($text, $marker);
991
992 $Excerpt = array('text' => $excerpt, 'context' => $text);
993
994 foreach ($this->InlineTypes[$marker] as $inlineType)
995 {
996 $Inline = $this->{'inline'.$inlineType}($Excerpt);
997
998 if ( ! isset($Inline))
999 {
1000 continue;
1001 }
1002
1003 # makes sure that the inline belongs to "our" marker
1004
1005 if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1006 {
1007 continue;
1008 }
1009
1010 # sets a default inline position
1011
1012 if ( ! isset($Inline['position']))
1013 {
1014 $Inline['position'] = $markerPosition;
1015 }
1016
1017 # the text that comes before the inline
1018 $unmarkedText = substr($text, 0, $Inline['position']);
1019
1020 # compile the unmarked text
1021 $markup .= $this->unmarkedText($unmarkedText);
1022
1023 # compile the inline
1024 $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1025
1026 # remove the examined text
1027 $text = substr($text, $Inline['position'] + $Inline['extent']);
1028
1029 continue 2;
1030 }
1031
1032 # the marker does not belong to an inline
1033
1034 $unmarkedText = substr($text, 0, $markerPosition + 1);
1035
1036 $markup .= $this->unmarkedText($unmarkedText);
1037
1038 $text = substr($text, $markerPosition + 1);
1039 }
1040
1041 $markup .= $this->unmarkedText($text);
1042
1043 return $markup;
1044 }
1045
1046 #
1047 # ~
1048 #
1049
1050 protected function inlineCode($Excerpt)
1051 {
1052 $marker = $Excerpt['text'][0];
1053
1054 if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1055 {
1056 $text = $matches[2];
1057 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1058 $text = preg_replace("/[ ]*\n/", ' ', $text);
1059
1060 return array(
1061 'extent' => strlen($matches[0]),
1062 'element' => array(
1063 'name' => 'code',
1064 'text' => $text,
1065 ),
1066 );
1067 }
1068 }
1069
1070 protected function inlineEmailTag($Excerpt)
1071 {
1072 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1073 {
1074 $url = $matches[1];
1075
1076 if ( ! isset($matches[2]))
1077 {
1078 $url = 'mailto:' . $url;
1079 }
1080
1081 return array(
1082 'extent' => strlen($matches[0]),
1083 'element' => array(
1084 'name' => 'a',
1085 'text' => $matches[1],
1086 'attributes' => array(
1087 'href' => $url,
1088 ),
1089 ),
1090 );
1091 }
1092 }
1093
1094 protected function inlineEmphasis($Excerpt)
1095 {
1096 if ( ! isset($Excerpt['text'][1]))
1097 {
1098 return;
1099 }
1100
1101 $marker = $Excerpt['text'][0];
1102
1103 if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1104 {
1105 $emphasis = 'strong';
1106 }
1107 elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1108 {
1109 $emphasis = 'em';
1110 }
1111 else
1112 {
1113 return;
1114 }
1115
1116 return array(
1117 'extent' => strlen($matches[0]),
1118 'element' => array(
1119 'name' => $emphasis,
1120 'handler' => 'line',
1121 'text' => $matches[1],
1122 ),
1123 );
1124 }
1125
1126 protected function inlineEscapeSequence($Excerpt)
1127 {
1128 if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1129 {
1130 return array(
1131 'markup' => $Excerpt['text'][1],
1132 'extent' => 2,
1133 );
1134 }
1135 }
1136
1137 protected function inlineImage($Excerpt)
1138 {
1139 if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1140 {
1141 return;
1142 }
1143
1144 $Excerpt['text']= substr($Excerpt['text'], 1);
1145
1146 $Link = $this->inlineLink($Excerpt);
1147
1148 if ($Link === null)
1149 {
1150 return;
1151 }
1152
1153 $Inline = array(
1154 'extent' => $Link['extent'] + 1,
1155 'element' => array(
1156 'name' => 'img',
1157 'attributes' => array(
1158 'src' => $Link['element']['attributes']['href'],
1159 'alt' => $Link['element']['text'],
1160 ),
1161 ),
1162 );
1163
1164 $Inline['element']['attributes'] += $Link['element']['attributes'];
1165
1166 unset($Inline['element']['attributes']['href']);
1167
1168 return $Inline;
1169 }
1170
1171 protected function inlineLink($Excerpt)
1172 {
1173 $Element = array(
1174 'name' => 'a',
1175 'handler' => 'line',
1176 'text' => null,
1177 'attributes' => array(
1178 'href' => null,
1179 'title' => null,
1180 ),
1181 );
1182
1183 $extent = 0;
1184
1185 $remainder = $Excerpt['text'];
1186
1187 if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
1188 {
1189 $Element['text'] = $matches[1];
1190
1191 $extent += strlen($matches[0]);
1192
1193 $remainder = substr($remainder, $extent);
1194 }
1195 else
1196 {
1197 return;
1198 }
1199
1200 if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
1201 {
1202 $Element['attributes']['href'] = $matches[1];
1203
1204 if (isset($matches[2]))
1205 {
1206 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1207 }
1208
1209 $extent += strlen($matches[0]);
1210 }
1211 else
1212 {
1213 if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1214 {
1215 $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1216 $definition = strtolower($definition);
1217
1218 $extent += strlen($matches[0]);
1219 }
1220 else
1221 {
1222 $definition = strtolower($Element['text']);
1223 }
1224
1225 if ( ! isset($this->DefinitionData['Reference'][$definition]))
1226 {
1227 return;
1228 }
1229
1230 $Definition = $this->DefinitionData['Reference'][$definition];
1231
1232 $Element['attributes']['href'] = $Definition['url'];
1233 $Element['attributes']['title'] = $Definition['title'];
1234 }
1235
1236 $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1237
1238 return array(
1239 'extent' => $extent,
1240 'element' => $Element,
1241 );
1242 }
1243
1244 protected function inlineMarkup($Excerpt)
1245 {
1246 if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1247 {
1248 return;
1249 }
1250
1251 if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1252 {
1253 return array(
1254 'markup' => $matches[0],
1255 'extent' => strlen($matches[0]),
1256 );
1257 }
1258
1259 if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1260 {
1261 return array(
1262 'markup' => $matches[0],
1263 'extent' => strlen($matches[0]),
1264 );
1265 }
1266
1267 if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1268 {
1269 return array(
1270 'markup' => $matches[0],
1271 'extent' => strlen($matches[0]),
1272 );
1273 }
1274 }
1275
1276 protected function inlineSpecialCharacter($Excerpt)
1277 {
1278 if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1279 {
1280 return array(
1281 'markup' => '&amp;',
1282 'extent' => 1,
1283 );
1284 }
1285
1286 $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1287
1288 if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1289 {
1290 return array(
1291 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1292 'extent' => 1,
1293 );
1294 }
1295 }
1296
1297 protected function inlineStrikethrough($Excerpt)
1298 {
1299 if ( ! isset($Excerpt['text'][1]))
1300 {
1301 return;
1302 }
1303
1304 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1305 {
1306 return array(
1307 'extent' => strlen($matches[0]),
1308 'element' => array(
1309 'name' => 'del',
1310 'text' => $matches[1],
1311 'handler' => 'line',
1312 ),
1313 );
1314 }
1315 }
1316
1317 protected function inlineUrl($Excerpt)
1318 {
1319 if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1320 {
1321 return;
1322 }
1323
1324 if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1325 {
1326 $Inline = array(
1327 'extent' => strlen($matches[0][0]),
1328 'position' => $matches[0][1],
1329 'element' => array(
1330 'name' => 'a',
1331 'text' => $matches[0][0],
1332 'attributes' => array(
1333 'href' => $matches[0][0],
1334 ),
1335 ),
1336 );
1337
1338 return $Inline;
1339 }
1340 }
1341
1342 protected function inlineUrlTag($Excerpt)
1343 {
1344 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1345 {
1346 $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1347
1348 return array(
1349 'extent' => strlen($matches[0]),
1350 'element' => array(
1351 'name' => 'a',
1352 'text' => $url,
1353 'attributes' => array(
1354 'href' => $url,
1355 ),
1356 ),
1357 );
1358 }
1359 }
1360
1361 # ~
1362
1363 protected function unmarkedText($text)
1364 {
1365 if ($this->breaksEnabled)
1366 {
1367 $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1368 }
1369 else
1370 {
1371 $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1372 $text = str_replace(" \n", "\n", $text);
1373 }
1374
1375 return $text;
1376 }
1377
1378 #
1379 # Handlers
1380 #
1381
1382 protected function element(array $Element)
1383 {
1384 $markup = '<'.$Element['name'];
1385
1386 if (isset($Element['attributes']))
1387 {
1388 foreach ($Element['attributes'] as $name => $value)
1389 {
1390 if ($value === null)
1391 {
1392 continue;
1393 }
1394
1395 $markup .= ' '.$name.'="'.$value.'"';
1396 }
1397 }
1398
1399 if (isset($Element['text']))
1400 {
1401 $markup .= '>';
1402
1403 if (isset($Element['handler']))
1404 {
1405 $markup .= $this->{$Element['handler']}($Element['text']);
1406 }
1407 else
1408 {
1409 $markup .= $Element['text'];
1410 }
1411
1412 $markup .= '</'.$Element['name'].'>';
1413 }
1414 else
1415 {
1416 $markup .= ' />';
1417 }
1418
1419 return $markup;
1420 }
1421
1422 protected function elements(array $Elements)
1423 {
1424 $markup = '';
1425
1426 foreach ($Elements as $Element)
1427 {
1428 $markup .= "\n" . $this->element($Element);
1429 }
1430
1431 $markup .= "\n";
1432
1433 return $markup;
1434 }
1435
1436 # ~
1437
1438 protected function li($lines)
1439 {
1440 $markup = $this->lines($lines);
1441
1442 $trimmedMarkup = trim($markup);
1443
1444 if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1445 {
1446 $markup = $trimmedMarkup;
1447 $markup = substr($markup, 3);
1448
1449 $position = strpos($markup, "</p>");
1450
1451 $markup = substr_replace($markup, '', $position, 4);
1452 }
1453
1454 return $markup;
1455 }
1456
1457 #
1458 # Deprecated Methods
1459 #
1460
1461 function parse($text)
1462 {
1463 $markup = $this->text($text);
1464
1465 return $markup;
1466 }
1467
1468 #
1469 # Static Methods
1470 #
1471
1472 static function instance($name = 'default')
1473 {
1474 if (isset(self::$instances[$name]))
1475 {
1476 return self::$instances[$name];
1477 }
1478
1479 $instance = new static();
1480
1481 self::$instances[$name] = $instance;
1482
1483 return $instance;
1484 }
1485
1486 private static $instances = array();
1487
1488 #
1489 # Fields
1490 #
1491
1492 protected $DefinitionData;
1493
1494 #
1495 # Read-Only
1496
1497 protected $specialCharacters = array(
1498 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1499 );
1500
1501 protected $StrongRegex = array(
1502 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1503 '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1504 );
1505
1506 protected $EmRegex = array(
1507 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1508 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1509 );
1510
1511 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1512
1513 protected $voidElements = array(
1514 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1515 );
1516
1517 protected $textLevelElements = array(
1518 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1519 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1520 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1521 'q', 'rt', 'ins', 'font', 'strong',
1522 's', 'tt', 'sub', 'mark',
1523 'u', 'xm', 'sup', 'nobr',
1524 'var', 'ruby',
1525 'wbr', 'span',
1526 'time',
1527 );
1528} \ No newline at end of file
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md
new file mode 100644
index 00000000..defaacd1
--- /dev/null
+++ b/plugins/markdown/README.md
@@ -0,0 +1,51 @@
1## Markdown Shaarli plugin
2
3Convert all your shaares description to HTML formatted Markdown.
4
5Read more about Markdown syntax here.
6
7### Installation
8
9Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there.
10The directory structure should look like:
11
12```
13??? plugins
14    ??? markdown
15 ??? help.html
16 ??? markdown.css
17 ??? markdown.meta
18 ??? markdown.php
19 ??? Parsedown.php
20    ??? README.md
21```
22
23To enable the plugin, add `markdown` to your list of enabled plugins in `data/config.php`
24(`ENABLED_PLUGINS` array).
25
26This should look like:
27
28```
29$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode', 'any_other_plugin', 'markdown')
30```
31
32### Known issue
33
34#### Redirector
35
36If you're using a redirector, you *need* to add a space after a link,
37otherwise the rest of the line will be `urlencode`.
38
39```
40[link](http://domain.tld)-->test
41```
42
43Will consider `http://domain.tld)-->test` as URL.
44
45Instead, add an additional space.
46
47```
48[link](http://domain.tld) -->test
49```
50
51> Won't fix because a `)` is a valid part of an URL.
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
new file mode 100644
index 00000000..9c4e5ae0
--- /dev/null
+++ b/plugins/markdown/help.html
@@ -0,0 +1,5 @@
1<div class="md_help">
2 Description will be rendered with
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation">
4 Markdown syntax</a>.
5</div>
diff --git a/plugins/markdown/markdown.css b/plugins/markdown/markdown.css
new file mode 100644
index 00000000..6d666dcf
--- /dev/null
+++ b/plugins/markdown/markdown.css
@@ -0,0 +1,137 @@
1/**
2 * Credit to Simon Laroche <https://github.com/simonlc/Markdown-CSS>
3 * whom created the CSS which this file is based on.
4 * License: Unlicense <http://unlicense.org/>
5 */
6
7.markdown p{
8 margin:0.75em 0;
9}
10
11.markdown img{
12 max-width:100%;
13}
14
15.markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6{
16 font-weight:normal;
17 font-style:normal;
18 line-height:1em;
19 margin:0.75em 0;
20}
21.markdown h4, .markdown h5, .markdown h6{ font-weight: bold; }
22.markdown h1{ font-size:2.5em; }
23.markdown h2{ font-size:2em; }
24.markdown h3{ font-size:1.5em; }
25.markdown h4{ font-size:1.2em; }
26.markdown h5{ font-size:1em; }
27.markdown h6{ font-size:0.9em; }
28
29.markdown blockquote{
30 color:#666666;
31 padding-left: 3em;
32 border-left: 0.5em #EEE solid;
33 margin:0.75em 0;
34}
35.markdown hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; }
36.markdown pre, .markdown code, .markdown kbd, .markdown samp {
37 font-family: monospace, 'courier new';
38 font-size: 0.98em;
39}
40.markdown pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
41
42.markdown b, .markdown strong { font-weight: bold; }
43
44.markdown dfn, .markdown em { font-style: italic; }
45
46.markdown ins { background: #ff9; color: #000; text-decoration: none; }
47
48.markdown mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
49
50.markdown sub, .markdown sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
51.markdown sup { top: -0.5em; }
52.markdown sub { bottom: -0.25em; }
53
54.markdown ul, .markdown ol { margin: 1em 0; padding: 0 0 0 2em; }
55.markdown li p:last-child { margin:0 }
56.markdown dd { margin: 0 0 0 2em; }
57
58.markdown img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
59
60.markdown table { border-collapse: collapse; border-spacing: 0; }
61.markdown td { vertical-align: top; }
62
63@media only screen and (min-width: 480px) {
64 .markdown {font-size:0.9em;}
65}
66
67@media only screen and (min-width: 768px) {
68 .markdown {font-size:1em;}
69}
70
71#linklist .markdown li {
72 padding: 0;
73 border: none;
74 background: none;
75}
76
77#linklist .markdown ul li {
78 list-style: circle;
79}
80
81#linklist .markdown ol li {
82 list-style: decimal;
83}
84
85.markdown table {
86 padding: 0;
87}
88.markdown table tr {
89 border-top: 1px solid #cccccc;
90 background-color: white;
91 margin: 0;
92 padding: 0;
93}
94.markdown table tr:nth-child(2n) {
95 background-color: #f8f8f8;
96}
97.markdown table tr th {
98 font-weight: bold;
99 border: 1px solid #cccccc;
100 text-align: left;
101 margin: 0;
102 padding: 6px 13px;
103}
104.markdown table tr td {
105 border: 1px solid #cccccc;
106 text-align: left;
107 margin: 0;
108 padding: 6px 13px;
109}
110.markdown table tr th :first-child, .markdown table tr td :first-child {
111 margin-top: 0;
112}
113.markdown table tr th :last-child, table tr td :last-child {
114 margin-bottom: 0;
115}
116
117.md_help {
118 color: white;
119}
120
121/*
122 Remove header links style
123 */
124#pageheader .md_help a {
125 color: lightgray;
126 font-weight: bold;
127 text-decoration: underline;
128
129 background: none;
130 box-shadow: none;
131 padding: 0;
132 margin: 0;
133}
134
135#pageheader .md_help a:hover {
136 color: white;
137}
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta
new file mode 100644
index 00000000..e3904ed8
--- /dev/null
+++ b/plugins/markdown/markdown.meta
@@ -0,0 +1 @@
description="Render shaare description with Markdown syntax."
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
new file mode 100644
index 00000000..3630ef14
--- /dev/null
+++ b/plugins/markdown/markdown.php
@@ -0,0 +1,158 @@
1<?php
2
3/**
4 * Plugin Markdown.
5 *
6 * Shaare's descriptions are parsed with Markdown.
7 */
8
9require_once 'Parsedown.php';
10
11/**
12 * Parse linklist descriptions.
13 *
14 * @param array $data linklist data.
15 *
16 * @return mixed linklist data parsed in markdown (and converted to HTML).
17 */
18function hook_markdown_render_linklist($data)
19{
20 foreach ($data['links'] as &$value) {
21 $value['description'] = process_markdown($value['description']);
22 }
23
24 return $data;
25}
26
27/**
28 * Parse daily descriptions.
29 *
30 * @param array $data daily data.
31 *
32 * @return mixed daily data parsed in markdown (and converted to HTML).
33 */
34function hook_markdown_render_daily($data)
35{
36 // Manipulate columns data
37 foreach ($data['cols'] as &$value) {
38 foreach ($value as &$value2) {
39 $value2['formatedDescription'] = process_markdown($value2['formatedDescription']);
40 }
41 }
42
43 return $data;
44}
45
46/**
47 * When link list is displayed, include markdown CSS.
48 *
49 * @param array $data includes data.
50 *
51 * @return mixed - includes data with markdown CSS file added.
52 */
53function hook_markdown_render_includes($data)
54{
55 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
56 || $data['_PAGE_'] == Router::$PAGE_DAILY
57 || $data['_PAGE_'] == Router::$PAGE_EDITLINK
58 ) {
59
60 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
61 }
62
63 return $data;
64}
65
66/**
67 * Hook render_editlink.
68 * Adds an help link to markdown syntax.
69 *
70 * @param array $data data passed to plugin
71 *
72 * @return array altered $data.
73 */
74function hook_markdown_render_editlink($data)
75{
76 // Load help HTML into a string
77 $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
78 return $data;
79}
80
81
82/**
83 * Remove HTML links auto generated by Shaarli core system.
84 * Keeps HREF attributes.
85 *
86 * @param string $description input description text.
87 *
88 * @return string $description without HTML links.
89 */
90function reverse_text2clickable($description)
91{
92 return preg_replace('!<a +href="([^ ]*)">[^ ]+</a>!m', '$1', $description);
93}
94
95/**
96 * Remove <br> tag to let markdown handle it.
97 *
98 * @param string $description input description text.
99 *
100 * @return string $description without <br> tags.
101 */
102function reverse_nl2br($description)
103{
104 return preg_replace('!<br */?>!im', '', $description);
105}
106
107/**
108 * Remove HTML spaces '&nbsp;' auto generated by Shaarli core system.
109 *
110 * @param string $description input description text.
111 *
112 * @return string $description without HTML links.
113 */
114function reverse_space2nbsp($description)
115{
116 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
117}
118
119/**
120 * Remove '&gt;' at start of line auto generated by Shaarli core system
121 * to allow markdown blockquotes.
122 *
123 * @param string $description input description text.
124 *
125 * @return string $description without HTML links.
126 */
127function reset_quote_tags($description)
128{
129 return preg_replace('/^( *)&gt; /m', '$1> ', $description);
130}
131
132/**
133 * Render shaare contents through Markdown parser.
134 * 1. Remove HTML generated by Shaarli core.
135 * 2. Generate markdown descriptions.
136 * 3. Wrap description in 'markdown' CSS class.
137 *
138 * @param string $description input description text.
139 *
140 * @return string HTML processed $description.
141 */
142function process_markdown($description)
143{
144 $parsedown = new Parsedown();
145
146 $processedDescription = $description;
147 $processedDescription = reverse_text2clickable($processedDescription);
148 $processedDescription = reverse_nl2br($processedDescription);
149 $processedDescription = reverse_space2nbsp($processedDescription);
150 $processedDescription = reset_quote_tags($processedDescription);
151 $processedDescription = $parsedown
152 ->setMarkupEscaped(false)
153 ->setBreaksEnabled(true)
154 ->text($processedDescription);
155 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
156
157 return $processedDescription;
158}
diff --git a/plugins/playvideos/playvideos.meta b/plugins/playvideos/playvideos.meta
new file mode 100644
index 00000000..c2b0908e
--- /dev/null
+++ b/plugins/playvideos/playvideos.meta
@@ -0,0 +1 @@
description="Add a button in the toolbar allowing to watch all videos."
diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta
new file mode 100644
index 00000000..cbf371ea
--- /dev/null
+++ b/plugins/qrcode/qrcode.meta
@@ -0,0 +1 @@
description="For each link, add a QRCode icon ."
diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta
new file mode 100644
index 00000000..bd611dd0
--- /dev/null
+++ b/plugins/readityourself/readityourself.meta
@@ -0,0 +1,2 @@
1description="For each link, add a ReadItYourself icon to save the shaared URL."
2parameters=READITYOUSELF_URL; \ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
index 1b030bc8..c8df4c4f 100644
--- a/plugins/readityourself/readityourself.php
+++ b/plugins/readityourself/readityourself.php
@@ -13,7 +13,7 @@ if (is_file(PluginManager::$PLUGINS_PATH . '/readityourself/config.php')) {
13 include PluginManager::$PLUGINS_PATH . '/readityourself/config.php'; 13 include PluginManager::$PLUGINS_PATH . '/readityourself/config.php';
14} 14}
15 15
16if (!isset($GLOBALS['plugins']['READITYOUSELF_URL'])) { 16if (empty($GLOBALS['plugins']['READITYOUSELF_URL'])) {
17 $GLOBALS['plugin_errors'][] = 'Readityourself plugin error: '. 17 $GLOBALS['plugin_errors'][] = 'Readityourself plugin error: '.
18 'Please define "$GLOBALS[\'plugins\'][\'READITYOUSELF_URL\']" '. 18 'Please define "$GLOBALS[\'plugins\'][\'READITYOUSELF_URL\']" '.
19 'in "plugins/readityourself/config.php" or in your Shaarli config.php file.'; 19 'in "plugins/readityourself/config.php" or in your Shaarli config.php file.';
diff --git a/plugins/wallabag/wallabag.meta b/plugins/wallabag/wallabag.meta
new file mode 100644
index 00000000..8763c4a2
--- /dev/null
+++ b/plugins/wallabag/wallabag.meta
@@ -0,0 +1,2 @@
1description="For each link, add a Wallabag icon to save it in your instance."
2parameters="WALLABAG_URL" \ No newline at end of file
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index e3c399a9..0d6fc66d 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -11,7 +11,7 @@ if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) {
11 include PluginManager::$PLUGINS_PATH . '/wallabag/config.php'; 11 include PluginManager::$PLUGINS_PATH . '/wallabag/config.php';
12} 12}
13 13
14if (!isset($GLOBALS['plugins']['WALLABAG_URL'])) { 14if (empty($GLOBALS['plugins']['WALLABAG_URL'])) {
15 $GLOBALS['plugin_errors'][] = 'Wallabag plugin error: '. 15 $GLOBALS['plugin_errors'][] = 'Wallabag plugin error: '.
16 'Please define "$GLOBALS[\'plugins\'][\'WALLABAG_URL\']" '. 16 'Please define "$GLOBALS[\'plugins\'][\'WALLABAG_URL\']" '.
17 'in "plugins/wallabag/config.php" or in your Shaarli config.php file.'; 17 'in "plugins/wallabag/config.php" or in your Shaarli config.php file.';
diff --git a/shaarli_version.php b/shaarli_version.php
index fe5f3896..99181e3e 100644
--- a/shaarli_version.php
+++ b/shaarli_version.php
@@ -1 +1 @@
<?php /* 0.6.2 */ ?> <?php /* 0.6.3 */ ?>
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php
index adebfcc3..492ddd3b 100644
--- a/tests/ConfigTest.php
+++ b/tests/ConfigTest.php
@@ -174,4 +174,113 @@ class ConfigTest extends PHPUnit_Framework_TestCase
174 include self::$configFields['config']['CONFIG_FILE']; 174 include self::$configFields['config']['CONFIG_FILE'];
175 $this->assertEquals(self::$configFields['login'], $GLOBALS['login']); 175 $this->assertEquals(self::$configFields['login'], $GLOBALS['login']);
176 } 176 }
177
178 /**
179 * Test save_plugin_config with valid data.
180 *
181 * @throws PluginConfigOrderException
182 */
183 public function testSavePluginConfigValid()
184 {
185 $data = array(
186 'order_plugin1' => 2, // no plugin related
187 'plugin2' => 0, // new - at the end
188 'plugin3' => 0, // 2nd
189 'order_plugin3' => 8,
190 'plugin4' => 0, // 1st
191 'order_plugin4' => 5,
192 );
193
194 $expected = array(
195 'plugin3',
196 'plugin4',
197 'plugin2',
198 );
199
200 $out = save_plugin_config($data);
201 $this->assertEquals($expected, $out);
202 }
203
204 /**
205 * Test save_plugin_config with invalid data.
206 *
207 * @expectedException PluginConfigOrderException
208 */
209 public function testSavePluginConfigInvalid()
210 {
211 $data = array(
212 'plugin2' => 0,
213 'plugin3' => 0,
214 'order_plugin3' => 0,
215 'plugin4' => 0,
216 'order_plugin4' => 0,
217 );
218
219 save_plugin_config($data);
220 }
221
222 /**
223 * Test save_plugin_config without data.
224 */
225 public function testSavePluginConfigEmpty()
226 {
227 $this->assertEquals(array(), save_plugin_config(array()));
228 }
229
230 /**
231 * Test validate_plugin_order with valid data.
232 */
233 public function testValidatePluginOrderValid()
234 {
235 $data = array(
236 'order_plugin1' => 2,
237 'plugin2' => 0,
238 'plugin3' => 0,
239 'order_plugin3' => 1,
240 'plugin4' => 0,
241 'order_plugin4' => 5,
242 );
243
244 $this->assertTrue(validate_plugin_order($data));
245 }
246
247 /**
248 * Test validate_plugin_order with invalid data.
249 */
250 public function testValidatePluginOrderInvalid()
251 {
252 $data = array(
253 'order_plugin1' => 2,
254 'order_plugin3' => 1,
255 'order_plugin4' => 1,
256 );
257
258 $this->assertFalse(validate_plugin_order($data));
259 }
260
261 /**
262 * Test load_plugin_parameter_values.
263 */
264 public function testLoadPluginParameterValues()
265 {
266 $plugins = array(
267 'plugin_name' => array(
268 'parameters' => array(
269 'param1' => true,
270 'param2' => false,
271 'param3' => '',
272 )
273 )
274 );
275
276 $parameters = array(
277 'param1' => 'value1',
278 'param2' => 'value2',
279 );
280
281 $result = load_plugin_parameter_values($plugins, $parameters);
282 $this->assertEquals('value1', $result['plugin_name']['parameters']['param1']);
283 $this->assertEquals('value2', $result['plugin_name']['parameters']['param2']);
284 $this->assertEquals('', $result['plugin_name']['parameters']['param3']);
285 }
177} 286}
diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php
index df2614b5..348082c7 100644
--- a/tests/PluginManagerTest.php
+++ b/tests/PluginManagerTest.php
@@ -63,4 +63,23 @@ class PluginManagerTest extends PHPUnit_Framework_TestCase
63 63
64 $pluginManager->load(array('nope', 'renope')); 64 $pluginManager->load(array('nope', 'renope'));
65 } 65 }
66
67 /**
68 * Test plugin metadata loading.
69 */
70 public function testGetPluginsMeta()
71 {
72 $pluginManager = PluginManager::getInstance();
73
74 PluginManager::$PLUGINS_PATH = self::$pluginPath;
75 $pluginManager->load(array(self::$pluginName));
76
77 $expectedParameters = array(
78 'pop' => '',
79 'hip' => '',
80 );
81 $meta = $pluginManager->getPluginsMeta();
82 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
83 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
84 }
66} \ No newline at end of file 85} \ No newline at end of file
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
new file mode 100644
index 00000000..455f5ba7
--- /dev/null
+++ b/tests/plugins/PluginMarkdownTest.php
@@ -0,0 +1,112 @@
1<?php
2
3/**
4 * PluginMarkdownTest.php
5 */
6
7require_once 'application/Utils.php';
8require_once 'plugins/markdown/markdown.php';
9
10/**
11 * Class PlugQrcodeTest
12 * Unit test for the QR-Code plugin
13 */
14class PluginMarkdownTest extends PHPUnit_Framework_TestCase
15{
16 /**
17 * Reset plugin path
18 */
19 function setUp()
20 {
21 PluginManager::$PLUGINS_PATH = 'plugins';
22 }
23
24 /**
25 * Test render_linklist hook.
26 * Only check that there is basic markdown rendering.
27 */
28 function testMarkdownLinklist()
29 {
30 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
31 $data = array(
32 'links' => array(
33 0 => array(
34 'description' => $markdown,
35 ),
36 ),
37 );
38
39 $data = hook_markdown_render_linklist($data);
40 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
41 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
42 }
43
44 /**
45 * Test render_daily hook.
46 * Only check that there is basic markdown rendering.
47 */
48 function testMarkdownDaily()
49 {
50 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
51 $data = array(
52 // Columns data
53 'cols' => array(
54 // First, second, third.
55 0 => array(
56 // nth link
57 0 => array(
58 'formatedDescription' => $markdown,
59 ),
60 ),
61 ),
62 );
63
64 $data = hook_markdown_render_daily($data);
65 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<h1>'));
66 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<p>'));
67 }
68
69 /**
70 * Test reverse_text2clickable().
71 */
72 function testReverseText2clickable()
73 {
74 $text = 'stuff http://hello.there/is=someone#here otherstuff';
75 $clickableText = text2clickable($text, '');
76 $reversedText = reverse_text2clickable($clickableText);
77 $this->assertEquals($text, $reversedText);
78 }
79
80 /**
81 * Test reverse_nl2br().
82 */
83 function testReverseNl2br()
84 {
85 $text = 'stuff' . PHP_EOL . 'otherstuff';
86 $processedText = nl2br($text);
87 $reversedText = reverse_nl2br($processedText);
88 $this->assertEquals($text, $reversedText);
89 }
90
91 /**
92 * Test reverse_space2nbsp().
93 */
94 function testReverseSpace2nbsp()
95 {
96 $text = ' stuff' . PHP_EOL . ' otherstuff and another';
97 $processedText = space2nbsp($text);
98 $reversedText = reverse_space2nbsp($processedText);
99 $this->assertEquals($text, $reversedText);
100 }
101
102 /**
103 * Test reset_quote_tags()
104 */
105 function testResetQuoteTags()
106 {
107 $text = '> quote1'. PHP_EOL . ' > quote2 ' . PHP_EOL . 'noquote';
108 $processedText = escape($text);
109 $reversedText = reset_quote_tags($processedText);
110 $this->assertEquals($text, $reversedText);
111 }
112}
diff --git a/tests/plugins/test/test.meta b/tests/plugins/test/test.meta
new file mode 100644
index 00000000..ab999ed4
--- /dev/null
+++ b/tests/plugins/test/test.meta
@@ -0,0 +1,2 @@
1description="test plugin"
2parameters="pop;hip" \ No newline at end of file
diff --git a/tpl/404.html b/tpl/404.html
index a45be801..53e98e2e 100644
--- a/tpl/404.html
+++ b/tpl/404.html
@@ -10,7 +10,7 @@
10<div class="error-container"> 10<div class="error-container">
11 <h1>404 Not found <small>Oh crap!</small></h1> 11 <h1>404 Not found <small>Oh crap!</small></h1>
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
13 <p>Would you mind <a href="?">clicking here</a> ?</p> 13 <p>Would you mind <a href="?">clicking here</a>?</p>
14</div> 14</div>
15{include="page.footer"} 15{include="page.footer"}
16</body> 16</body>
diff --git a/tpl/pluginsadmin.html b/tpl/pluginsadmin.html
new file mode 100644
index 00000000..4f7d091e
--- /dev/null
+++ b/tpl/pluginsadmin.html
@@ -0,0 +1,131 @@
1<!DOCTYPE html>
2<html>
3<head>{include="includes"}</head>
4<body>
5<div id="pageheader">
6 {include="page.header"}
7</div>
8
9<noscript>
10 <div>
11 <ul class="errors">
12 <li>You need to enable Javascript to change plugin loading order.</li>
13 </ul>
14 </div>
15 <div class="clear"></div>
16</noscript>
17
18<div id="pluginsadmin">
19 <form action="?do=save_pluginadmin" method="POST">
20 <section id="enabled_plugins">
21 <h1>Enabled Plugins</h1>
22
23 <div>
24 {if="count($enabledPlugins)==0"}
25 <p>No plugin enabled.</p>
26 {else}
27 <table id="plugin_table">
28 <thead>
29 <tr>
30 <th class="center">Disable</th>
31 <th class="center">Order</th>
32 <th>Name</th>
33 <th>Description</th>
34 </tr>
35 </thead>
36 <tbody>
37 {loop="$enabledPlugins"}
38 <tr data-line="{$key}" data-order="{$counter}">
39 <td class="center"><input type="checkbox" name="{$key}" checked="checked"></td>
40 <td class="center">
41 <a href="#"
42 onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));">
43 â–²
44 </a>
45 <a href="#"
46 onclick="return orderDown(this.parentNode.parentNode.getAttribute('data-order'));">
47 â–¼
48 </a>
49 <input type="hidden" name="order_{$key}" value="{$counter}">
50 </td>
51 <td>{$key}</td>
52 <td>{$value.description}</td>
53 </tr>
54 {/loop}
55 </tbody>
56 </table>
57 {/if}
58 </div>
59 </section>
60
61 <section id="disabled_plugins">
62 <h1>Disabled Plugins</h1>
63
64 <div>
65 {if="count($disabledPlugins)==0"}
66 <p>No plugin disabled.</p>
67 {else}
68 <table>
69 <tr>
70 <th class="center">Enable</th>
71 <th>Name</th>
72 <th>Description</th>
73 </tr>
74 {loop="$disabledPlugins"}
75 <tr>
76 <td class="center"><input type="checkbox" name="{$key}"></td>
77 <td>{$key}</td>
78 <td>{$value.description}</td>
79 </tr>
80 {/loop}
81 </table>
82 {/if}
83 </div>
84
85 <div class="center">
86 <input type="submit" value="Save"/>
87 </div>
88 </section>
89 </form>
90
91 <form action="?do=save_pluginadmin" method="POST">
92 <section id="plugin_parameters">
93 <h1>Enabled Plugin Parameters</h1>
94
95 <div>
96 {if="count($enabledPlugins)==0"}
97 <p>No plugin enabled.</p>
98 {else}
99 {loop="$enabledPlugins"}
100 {if="count($value.parameters) > 0"}
101 <div class="plugin_parameters">
102 <h2>{$key}</h2>
103 {loop="$value.parameters"}
104 <div class="plugin_parameter">
105 <div class="float_label">
106 <label for="{$key}">
107 <code>{$key}</code>
108 </label>
109 </div>
110 <div class="float_input">
111 <input name="{$key}" value="{$value}" id="{$key}"/>
112 </div>
113 </div>
114 {/loop}
115 </div>
116 {/if}
117 {/loop}
118 {/if}
119 <div class="center">
120 <input type="submit" name="parameters_form" value="Save"/>
121 </div>
122 </div>
123 </section>
124 </form>
125
126</div>
127{include="page.footer"}
128
129<script src="inc/plugin_admin.js#"></script>
130</body>
131</html> \ No newline at end of file
diff --git a/tpl/tools.html b/tpl/tools.html
index c13f4f16..78b81663 100644
--- a/tpl/tools.html
+++ b/tpl/tools.html
@@ -5,11 +5,18 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b> <span>: Change your password.</span></a><br><br>{/if} 8 <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
9 <a href="?do=configure"><b>Configure your Shaarli</b> <span>: Change Title, timezone...</span></a><br><br> 9 <br><br>
10 <a href="?do=changetag"><b>Rename/delete tags</b> <span>: Rename or delete a tag in all links</span></a><br><br> 10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <a href="?do=import"><b>Import</b> <span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> <br><br> 11 <br><br>
12 <a href="?do=export"><b>Export</b> <span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a><br><br> 12 {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if}
14 <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
15 <br><br>
16 <a href="?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
17 <br><br>
18 <a href="?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
19 <br><br>
13 <a class="smallbutton" 20 <a class="smallbutton"
14 onclick="return alertBookmarklet();" 21 onclick="return alertBookmarklet();"
15 href="javascript:( 22 href="javascript:(