diff options
31 files changed, 2636 insertions, 20 deletions
@@ -72,6 +72,10 @@ Files: plugins/wallabag/wallabag.png | |||
72 | License: MIT License (http://opensource.org/licenses/MIT) | 72 | License: MIT License (http://opensource.org/licenses/MIT) |
73 | Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag | 73 | Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag |
74 | 74 | ||
75 | Files: plugins/markdown/Parsedown.php | ||
76 | License: MIT License (http://opensource.org/licenses/MIT) | ||
77 | Copyright: (C) 2015 Emanuil Rusev - https://github.com/erusev/parsedown | ||
78 | |||
75 | ---------------------------------------------------- | 79 | ---------------------------------------------------- |
76 | ZLIB/LIBPNG LICENSE | 80 | ZLIB/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 | */ | ||
85 | function 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 | */ | ||
133 | function 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 | */ | ||
158 | function 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 | */ | ||
239 | class 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 | */ | ||
67 | function nl2br_escaped($html) | ||
68 | { | ||
69 | return str_replace('>', '>', str_replace('<', '<', nl2br($html))); | ||
70 | } | ||
71 | |||
72 | /** | ||
73 | * htmlspecialchars wrapper | 65 | * htmlspecialchars wrapper |
74 | */ | 66 | */ |
75 | function escape($str) | 67 | function 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 | */ | ||
7 | function 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 | */ | ||
22 | function 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 | */ | ||
42 | function 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 | */ | ||
58 | function 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 | ||
@@ -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 | */ |
122 | define('shaarli_version', '0.6.2'); | 122 | define('shaarli_version', '0.6.3'); |
123 | 123 | ||
124 | // http://server.com/x/shaarli --> /shaarli/ | 124 | // http://server.com/x/shaarli --> /shaarli/ |
125 | define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); | 125 | define('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 | |||
16 | class 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('&', '<'), $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' => '&', | ||
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('&', '<'), $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 | |||
3 | Convert all your shaares description to HTML formatted Markdown. | ||
4 | |||
5 | Read more about Markdown syntax here. | ||
6 | |||
7 | ### Installation | ||
8 | |||
9 | Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. | ||
10 | The 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 | |||
23 | To enable the plugin, add `markdown` to your list of enabled plugins in `data/config.php` | ||
24 | (`ENABLED_PLUGINS` array). | ||
25 | |||
26 | This 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 | |||
36 | If you're using a redirector, you *need* to add a space after a link, | ||
37 | otherwise the rest of the line will be `urlencode`. | ||
38 | |||
39 | ``` | ||
40 | [link](http://domain.tld)-->test | ||
41 | ``` | ||
42 | |||
43 | Will consider `http://domain.tld)-->test` as URL. | ||
44 | |||
45 | Instead, 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 | |||
9 | require_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 | */ | ||
18 | function 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 | */ | ||
34 | function 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 | */ | ||
53 | function 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 | */ | ||
74 | function 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 | */ | ||
90 | function 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 | */ | ||
102 | function reverse_nl2br($description) | ||
103 | { | ||
104 | return preg_replace('!<br */?>!im', '', $description); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * Remove HTML spaces ' ' auto generated by Shaarli core system. | ||
109 | * | ||
110 | * @param string $description input description text. | ||
111 | * | ||
112 | * @return string $description without HTML links. | ||
113 | */ | ||
114 | function reverse_space2nbsp($description) | ||
115 | { | ||
116 | return preg_replace('/(^| ) /m', '$1 ', $description); | ||
117 | } | ||
118 | |||
119 | /** | ||
120 | * Remove '>' 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 | */ | ||
127 | function reset_quote_tags($description) | ||
128 | { | ||
129 | return preg_replace('/^( *)> /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 | */ | ||
142 | function 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 @@ | |||
1 | description="For each link, add a ReadItYourself icon to save the shaared URL." | ||
2 | parameters=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 | ||
16 | if (!isset($GLOBALS['plugins']['READITYOUSELF_URL'])) { | 16 | if (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 @@ | |||
1 | description="For each link, add a Wallabag icon to save it in your instance." | ||
2 | parameters="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 | ||
14 | if (!isset($GLOBALS['plugins']['WALLABAG_URL'])) { | 14 | if (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 | |||
7 | require_once 'application/Utils.php'; | ||
8 | require_once 'plugins/markdown/markdown.php'; | ||
9 | |||
10 | /** | ||
11 | * Class PlugQrcodeTest | ||
12 | * Unit test for the QR-Code plugin | ||
13 | */ | ||
14 | class 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 @@ | |||
1 | description="test plugin" | ||
2 | parameters="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:( |