diff --git a/.gitignore b/.gitignore
index e7c7906..2e1ff73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
# Ignore generated log files.
/bin/*.log
-/bin/token.test
\ No newline at end of file
+/bin/token.test
+_config.php
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e69de29
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..443d190
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,22 @@
+
+
+
+RewriteEngine On
+RewriteBase /
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(transmogrify/(?:info|system|tests))/(.*)\.(.*)$ /lib/delegator-index.php?r=$2&t=$3&c=$1 [L]
+RewriteRule ^(transmogrify/(?:info|system|tests))/$ /lib/delegator-index.php?r=$1&t=LIST [L]
+RewriteRule ^(transmogrify/.*)\.(.*)$ /lib/delegator-index.php?r=$1&t=$2 [L]
+RewriteRule ^(transmogrify/.*)$ /lib/delegator-index.php?r=$1&t=LIST [L]
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ /docs/index.php?presto&page=$1 [NC,QSA,L]
+
+
+
+ErrorDocument 404 /docs/index.php?presto&error=404
+ErrorDocument 500 /docs/index.php?presto&error=500
+ErrorDocument 401 /docs/index.php?presto&error=401
+ErrorDocument 403 /docs/index.php?presto&error=403
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..863a500
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,122 @@
+# Changelog
+
+# 1.1 - *Sim Sala Bim*
+
+**Presto 1.1 breaks compatibility with 1.0 in a few important areas:**
+
+* Adds subfolder delegation
+* Removes add_delegate*, URL, and other unused code (subfolder delegation is much simpler)
+* Improves debugging (PHP errors to error log, added trace mode)
+* Added unit tests
+* Updated setup tests (to use internal API)
+* `API` base startup has been simplified, removing the class pre-scanning mechanism. Presto will throw an exception if 1.0 usage is detected.
+* API parameters are flattened to their obvious parts (get parameters, options, put/post body, resource type). This deprecates the `$ctx` object, which is still available via `self::$call`.
+* `add_delegate` has been removed, as it has been replaced by folder delegation
+* `add_filter` has been removed (a candidate 1.1 feature), as the other features provided enough utility on their own
+* `Request::URI` has been removed in favour of a simpler builder function. URI parsing is now spread over `htaccess` for more obvious routing rules.
+
+## Alpha 1 features
+
+This patch adds a number of features planned for 1.1, including:
+
+* Dispatching to folders of APIs
+* Preflighting dispatch calls for parameter and model processing
+* Adds internal testing and debugging APIs (only available when enabled)
+* Adds command line unit testing tools
+* Adds a trace mode, and better exception logging
+* Adds routing information to exceptions (if trace is enabled)
+
+## Updated parameter passing
+
+The calling convention for delegate API functions has changed from:
+
+ public function get($ctx) { }
+ public function get_article($ctx) { }
+ public function put_article($ctx) { }
+
+To the more verbose form:
+
+ public function get($params, $options, $body, $type) { }
+ public function get_article($params, $options, $body, $type) { }
+ public function put_article($params, $options, $body, $type) { }
+
+This makes the call parameters clearer than they were in the past.
+
+## Example folder delegation
+
+Folder delegation uses Apache rewriting to map folders of APIs to specific URLs. For example, Presto uses it for internal delegation to its `transmogrify` APIs:
+
+ RewriteRule ^(transmogrify/(?:info|system|tests))/(.*)\.(.*)$ /lib/delegator-index.php?r=$2&t=$3&c=$1 [L]
+
+This routes all requests to 3 specific folders within `transmogrify` to files within the folders `info/`, `system/`, and `tests/` in `transmogrify/`.
+
+Requests like:
+
+ get "transmogrify/system/httpd/headers/content-type.json"
+
+Are mapped to:
+
+ transmogrify/system/httpd.php::httpd->get_headers();
+
+## Example preflight delegation
+
+API calls can be split into two parts to simplify handling complex model creation and parameter processing. The split adds a preflight call to a *model* function of the same root name as the API itself.
+
+Consider a simple GET:
+
+ get "transmogrify/system/httpd/headers/content-type.json"
+
+This call can optionally be split into the model creation and the API call itself.
+
+The model creation preflight call would be:
+
+ http.php::http->get_headers_model($p = array('content-type')) {
+ return new Features($p);
+ }
+
+The call receives the standard API request parameters, returning a newly created model object of some sort. This call can be used to check parameters, check preconditions, and massage parameters into useful/predictable values for the API call itself:
+
+ http.php::http->get_headers($featureModel) {
+ }
+
+
+## Example routing trace
+
+If `PRESTO_TRACE` is enabled, exceptions will include:
+
+
+ [OPTIONS] transmogrify/info/delegation.json #0 [OK]
+ {
+ "_presto_trace": {
+ "request": "/transmogrify/info/delegation.json",
+ "routing_scheme": {
+ "action": "options",
+ "class": "delegation",
+ "container": "transmogrify/info",
+ "file": "transmogrify/info/delegation.php",
+ "method": "options",
+ "options": [],
+ "params": [],
+ "resource": "",
+ "type": "json"
+ },
+ "version": "presto-v1.10"
+ },
+ "code": 404,
+ "message": "Can't find delegation->options"
+ }
+
+
+The `_presto_trace` element is added to make debugging easier. This trace is also available in the error log (in trace mode only):
+
+ [Fri Feb 22 16:38:58 2013] [error] [client 127.0.0.1] PRESTO: _presto_trace, {"routing_scheme":{"container":"transmogrify\\/info","class":"delegation","file":"transmogrify\\/info\\/delegation.php","resource":"","type":"json","action":"get","method":"get","params":[],"options":[]},"request":"\\/transmogrify\\/info\\/delegation.json","version":"presto-v1.10"}
+
+
+# 1.0 - A la peanut butter sandwiches
+
+* First public release
+* Adds sub delegates and param filtering
+
+# 0.8
+
+* Adds basic URI -> class/member delegation and auto data return by type
diff --git a/INSTALL.md b/INSTALL.md
index f1ce206..a9a0317 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -25,7 +25,7 @@ This file sets up Presto's delegator features, so that your classes are called i
4. Copy the example API file and test:
-
$ cp lib/presto/examples/info.php .
+
$ cp lib/presto/setup-tests/info.php .
$ curl [YOUR WEBROOT]/info.json
{"example":"This is some example information"}
@@ -41,4 +41,4 @@ The example API file writes a simple DOM (a `PHP` keyed array) as `JSON` back to
You need to enable the JSON extension using a custom RC (not a custom .ini). Find the instructions here:
-* http://wiki.dreamhost.com/PHP.ini#Loading_PHP_5.3_extensions_on_all_domains_.28on_VPS_or_dedicated.29
\ No newline at end of file
+* http://wiki.dreamhost.com/PHP.ini#Loading_PHP_5.3_extensions_on_all_domains_.28on_VPS_or_dedicated.29
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index a5c3517..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (C) 2010-2011 by Bruce Alderson
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..fa17ef1
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,12 @@
+# Free, free, share-alike
+
+Created by [Bruce Alderson](http://warpedvisions.org) (2011).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+**PS: use, extend, share, enjoy. Thanks!**
\ No newline at end of file
diff --git a/README.md b/README.md
index 1e87650..0edfd38 100644
--- a/README.md
+++ b/README.md
@@ -1,86 +1,81 @@
-Presto - PHP REST toolkit
-=========================
+# Presto 1.1
-Presto is a small library for building simple, RESTful APIs using PHP. It's lightweight, decoupled, and focused on making web apps the right way; using APIs with clean URLs, which produce simple, clean output in standard formats like JSON, HTML, and XML.
+*Frugal PHP + REST toolkit*
-How is it different?
---------------------
+Presto is a library for building RESTful APIs in PHP `5.3+`. It's lightweight, decoupled, and focused on making web apps the right way; using APIs and clean URLs to produce output in standard formats like JSON, HTML, and XML. It encourages separating views completely from your model and controller code, and to animate your user interfaces in HTML, CSS, and JavaScript. It also uses existing tools and libraries for what they're good at, like letting Apache deal with routing, and letting PHP load and execute a minimal amount of dynamic code.
-Presto has no views, no models, and no controllers[^1]. Instead it focuses on APIs built from simple classes. It encourages applications to be based on APIs, which feed web applications animated with HTML/CSS/JavaScript.
+## How is Presto different?
-A Presto API is simply a PHP class that maps to a resource or tree of resources. It has:
+Presto flattens standard MVC to fit REST requests better. It focuses on APIs built from simple classes, relying on the web server for routing and on PHP autoloading for delegating requests to class member calls. User interfaces are left to other toolkits, though Presto can serve up fragments of HTML with ease. Presto focuses on APIs.
-* Public members that map to requests (`GET thing/details.json` would map to `$thing->get_details()`)
-* Request parameters and input payloads that are packaged up and sanitized
-* Each route returns data as DOMs, which are automatically adapted to the requested `ContentType`
-* Errors are automatically converted into returned HTTP statuses
+### A quick example
-The resulting code is focused on resources and the rules for those resources, and not boilerplate, excessive error checking, routing, and output generation.
+An API is a class with members named for each resource (or tree of resources). For example, an `apple` is a resource. You can request an `apple` over HTTP:
-[^1]: Note that Presto avoids MVC by solving a smaller problem and relying on built in functionality for delegation and generating output.
+ GET fruit/apples/spartan.json?tags=large+red
-How it works
-------------
-Presto uses the best parts of PHP and Apache, as they're intended to be used. It relies on `.htaccess` for routing, and built-in PHP behaviours for delegation and class loading. The principle is that relying on existing, simple approaches results in less framework, fewer bugs, and less to learn that isn't useful elsewhere.
+Presto maps the request to a file, class, and class member automatically. It loads the file, creates an instance of the class, and executes the member that best fits:
-*Note: PHP 5.3 or better is requried, as Presto relies on anonymous functions, and other newer PHP features. This makes it possible to write very clean, simple web services.*
-An example API
---------------
-
- class info
- extends API {
-
- // Set up the API
- public function __construct() {
- parent::__construct(get_class());
- if (!self::isSignedIn()) throw new Exception('Auth required', 401);
- }
-
- // GET info/time.json - gets the local machine time
- public function get_time($ctx) {
- $dom = (object) getdate();
- return $dom; // <-- returns the DOM as JSON
+ /* Loaded from 'fruit/apples.php' */
+
+ class apples extends API {
+
+ public function get($params, $options, $body, $type) {
+
+ if (count($params) === 0)
+ throw new Exception('Missing required parameter', 400);
+
+ /* Exceptions are returned as valid HTTP/content type
+ responses */
+
+ $thing = (object) array(
+ 'name' => $params[0],
+ 'tags' => $options['tags']
+ );
+
+ return $thing; // to client in requested format
}
-
- // PUT info/time.json
- public function put_time($ctx) { throw new Exception('Not implemented', 501); }
- // POST info/time.json
- public function post_time($ctx) { throw new Exception('Not implemented', 501); }
}
-The class promises a GET, PUT, and POST interface for time. All other requests will return a standard 404. The PUT and POST are not implemented yet, so they return a 501 (not implemented).
-Presto maps requests to PHP files and objects, providing parameters and other calling information in the `$ctx` parameter. A request like:
+The `$thing` is automatically converted by Presto to the requested `Content-Type`, either implied by the request or the appropriate HTTP header. For formats not supported by default, *Output Adapters* can be defined and registered, or the type can be passed through for resources that are not based DOM style data.
- GET /info/time.json
-
-Maps to:
+Any HTTP request type is mapped automatically. For example, you can request a list of `apple` types available from the API:
- [info.php] info->get_time($ctx);
-
-The context includes the call parameters, the `content-type`, and request method. Handling of `content-type` is built in for simple cases, so that any data returned from an API is automatically transformed into the required type. For our simple example, returning the result of `datetime()` to the request produces:
-
- {
- "seconds" => 40
- "minutes" => 58
- "hours" => 21
- "mday" => 17
- "wday" => 2
- "mon" => 6
- "year" => 2003
- "yday" => 167
- "weekday" => "Tuesday"
- "month" => "June"
- "0" => 1055901520
- }
+ LIST apples.json?colour=red
+
+A LIST request is mapped to a function of the same name:
+
+ public function list(/* … */) { return array(); }
-Advanced features
------------------
-There are a number of ways you can extend Presto. You can:
+More complex resources are possible by either delegating (based on regex patterns), or by adding specific handlers. For example, you could add a `seeds` branch to the `apple` resource. Getting a list of seeds would map to:
+
+ public function list_seeds(/* … */) { array(); }
+
+### What about errors?
+
+Errors are handled by the toolkit as standard PHP exceptions and standard PHP errors are remapped (where possible) to exceptions to ensure that logic and code errors are returned to clients as valid API returns. This means that individual APIs do not need to check return values, nor do they need to `try` and `catch` unless they need to do something special. Presto maps the standard exceptions to HTTP statuses and output in whatever format was requested where possible.
+
+For example, if you encounter a parameter error you can simply throw an exception:
+
+ if (empty($param))
+ throw new Exception('Missing required parameter', 400);
+
+Presto translates the exception into a `400` with an appropriately encoded body.
+
+The resulting API code is much more focused on carefully testing parameters, retrieving appropriate resources, and building rich DOMs instead of boilerplate code, managing responses, excessive error checking, routing, and other complex output generation.
+
+
+Other interesting features
+==========================
+
+You can also:
* Add additional `content-types` by adding *output adapters* (`JSON`, simple `XML`, and simple `HTML` are built in)
* Add `content-type` filters to define what types of payloads a given resource supports.
* Add *custom delegation* for special resource types
+
+**Note that PHP 5.3 or better is required, as Presto relies on anonymous functions and other newer PHP features. This makes it possible to write very clean, simple web services.**
diff --git a/TODO.taskpaper b/TODO.taskpaper
deleted file mode 100644
index 454eb8a..0000000
--- a/TODO.taskpaper
+++ /dev/null
@@ -1,14 +0,0 @@
-Presto:
-
-- header exposure (nice, per type?)
-- Wrap Simple SSO library in a more reusable package
-Documentation:
-- Quick tutorial
-- FAQ
- - multiple deligates
- - limiting content types
-- per file/function
-Archive:
- - automated examples @done
- - New introduction @done
- - A better example @done
diff --git a/_tests/index.php b/_tests/index.php
deleted file mode 120000
index 712d49c..0000000
--- a/_tests/index.php
+++ /dev/null
@@ -1 +0,0 @@
-run-all-tests.php
\ No newline at end of file
diff --git a/_tests/run-all-tests.php b/_tests/run-all-tests.php
deleted file mode 100644
index bfbe139..0000000
--- a/_tests/run-all-tests.php
+++ /dev/null
@@ -1,187 +0,0 @@
-getCode()} - {$e->getMessage()}", 'FAIL');
- print("\n");
- print($e);
-
-}
-
-function response_complex_types_tests() {
- test("Response - complex types tests");
-
- $dom = << 'json' ) );
- $r->ok($dom);
- print "\n";
- status("JSON output for complex data", 'OK');
-
- $r = new Response((object) array( 'res' => 'htm' ) ); // htm intentional, tests match
- $r->ok($dom);
- print "\n";
- status("Default HTML output for complex data", 'OK');
-
- unset($r);
- $r = new Response((object) array( 'res' => 'htm' ) ); // htm intentional, tests type matching
-
- Response::add_type_handler('.*\/htm.*', _encode_html, function($k, &$n, &$a, $d) {
- static $parents = array();
- static $was = 0;
-
- // track parents
-
- if ($was >= $d) // traversing up tree
- for($i = 0; $i < ($was - $d) + 1; $i++) array_pop($parents);
-
- $parents[] = $k;
- $p_at = count($parents) - 2;
- $p = $p_at >= 0 ? $parents[$p_at] : '';
-
- // remap node name
-
- switch ($k) {
- case 'chapters': $k = 'div'; break;
- case 'title': $k = 'h'.($d-1); break;
- case 'ideas': $k = 'ul'; break;
- case 'li':
- if ($p === 'chapters') { // remap chapter LIs
- $k = 'section';
- // also pull out the ID as an attribute
- if (array_key_exists('id', $n)) {
- $a .= " id='{$n['id']}'";
- unset($n['id']);
- }
- }
- break;
- }
-
- $was = $d; // track depth for parents path
-
- return $k;
- } );
-
- $r->ok($dom);
- print "\n";
- status("Mapped HTML output for complex data", 'OK');
-
- // TODO custom mapper / handler
-}
-
-function response_tests() {
- test("Response tests");
- $r = new Response((object) array( 'res' => 'json' ) );
- status("Created response object", 'OK');
-
- $r->ok( array('test' => 'test data') );
- print "\n";
- status("Responded with simple JSON transform", 'OK');
-
- $r = new Response((object) array( 'res' => 'html' ) );
- $r->ok( array('test' => 'test data') );
- print "\n";
- status("Responded with simple HTML transform", 'OK');
-}
-
-/* Test view helper(s) */
-function simple_view_tests() {
- test("View tests");
-
- View::$root = realpath(__DIR__) . '/';
- $v = new View('test-view', array('name' => 'test'));
- status("Created simple view", 'OK');
-
-}
-
-/* Test PDO and wrapper(s) */
-function database_tests() {
- test("DB/PDO tests");
-
- $dsn = 'mysql:host=localhost;dbname=test';
- $user = 'test';
- $password = '12345';
- $config = array(
- PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
- );
-
- $db = new db($dsn, $user, $password, $config);
- status("Connected to `$dsn`", 'OK');
-
- $r = $db->select('SELECT * FROM Test');
- status("Simple select", 'OK');
- result($r);
-}
-
-/* Test service class */
-function service_tests() {
- test("Service tests");
-
- $config = array(
- 'service' => 'https://api.twitter.com/',
- 'extra' => '1',
- 'debug' => 0,
- 'referer' => 'presto/test-script',
- 'agent' => 'presto/1.0'
- );
-
- $service = new Service($config);
-
- status("Created test service", 'OK');
- //https://api.twitter.com/1/help/test.json
- $data = $service->get_help('test.json');
- status("Simple GET request", 'OK');
- result($data);
-}
-
-
-function test($text) {
- print "\n=[ $text ]========================================\n\n";
-}
-
-/* Display status (with some console highlighting) */
-function status($text, $status) {
- $status = strtoupper($status);
- $c = $status == 'OK' ? '42' : '41';
-
- print " \033[1;{$c};30m[ {$status} ]\033[0m\t\t$text\n";
-}
-/* Display detailed results */
-function result($t) { ?>
-
-----
-
-= var_dump( $t ) ?>
-
-----
-
-
-
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..53f4ebd
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,36 @@
+{
+ "name": "napkinware/presto",
+ "version": "1.0.0",
+ "description": "A simple RESTful API library for PHP.",
+ "keywords": ["REST", "framework", "PHP"],
+ "homepage": "http://presto.napkinware.com",
+ "support": {
+ "issues": "https://github.com/robotpony/Presto/issues"
+ },
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Bruce Alderson",
+ "email": "bruce@napkinware.com"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/robotpony/presto"
+ }
+ ],
+ "minimum-stability": "stable",
+ "require": {
+ "php": ">=5.3.15",
+ "ext-curl": ">=0",
+ "ext-fileinfo": ">=1.0.0",
+ "ext-date": ">=5.3.0",
+ "ext-json": ">=1.2.1",
+ "lib-openssl": ">=0.9",
+ "lib-curl": ">=7.24.0"
+ },
+ "require-dev": {
+
+ }
+}
\ No newline at end of file
diff --git a/docs/application-state.md b/docs/application-state.md
index e69de29..be735af 100644
--- a/docs/application-state.md
+++ b/docs/application-state.md
@@ -0,0 +1,2 @@
+Application state (AKA, sessions)
+---------------------------------
\ No newline at end of file
diff --git a/docs/configuration.md b/docs/configuration.md
index e69de29..b26e07e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -0,0 +1,2 @@
+Configuration
+-------------
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
index 2a40c26..313aa55 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -1,7 +1,5 @@
Frequently asked things
-=======================
-
-Document style: FAQ
+-----------------------
## Q: Why no MVC?
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/docs/favicon.png b/docs/favicon.png
new file mode 100644
index 0000000..2cd5843
Binary files /dev/null and b/docs/favicon.png differ
diff --git a/docs/generating-output.md b/docs/generating-output.md
index 7b3becf..0c660bb 100644
--- a/docs/generating-output.md
+++ b/docs/generating-output.md
@@ -1,5 +1,5 @@
Generating output
-==================
+-----------------
All Presto API functions return a simple DOM object. This object (or array) is translated into output using a type handler, which is selected by the requested `content-type`.
diff --git a/docs/inc.php b/docs/inc.php
new file mode 120000
index 0000000..028bea4
--- /dev/null
+++ b/docs/inc.php
@@ -0,0 +1 @@
+../lib/inc.php
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index c0cb9db..ca09874 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,9 +1,23 @@
-Presto for PHP - developer documentation
-========================================
+Developer documentation
+-----------------------
+[License](/LICENSE.html)
-Finding your way around
------------------------
+[Installation instructions](/INSTALL.html)
+
+[Frequently Asked Things (FAQ)](/docs/faq.html)
+
+[Application state](/docs/application-state.html)
+
+[Configuring Presto](/docs/configuration.html)
+
+[Output adapters](/docs/generating-output.html)
+
+
+Missing topics
+==============
+
+These are things that aren't quite covered in the documentation yet.
* API classes (how requests are mapped to members)
* Returning data to clients
diff --git a/docs/index.php b/docs/index.php
new file mode 100644
index 0000000..b2ca449
--- /dev/null
+++ b/docs/index.php
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+= $title ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/js/md.js b/docs/js/md.js
new file mode 100644
index 0000000..e69de29
diff --git a/docs/lib/markdown/License.text b/docs/lib/markdown/License.text
new file mode 100755
index 0000000..4d6bf8b
--- /dev/null
+++ b/docs/lib/markdown/License.text
@@ -0,0 +1,36 @@
+PHP Markdown & Extra
+Copyright (c) 2004-2009 Michel Fortin
+
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
diff --git a/docs/lib/markdown/markdown.php b/docs/lib/markdown/markdown.php
new file mode 100755
index 0000000..f548fc2
--- /dev/null
+++ b/docs/lib/markdown/markdown.php
@@ -0,0 +1,2932 @@
+
+#
+# Original Markdown
+# Copyright (c) 2004-2006 John Gruber
+#
+#
+
+
+define( 'MARKDOWN_VERSION', "1.0.1o" ); # Sun 8 Jan 2012
+define( 'MARKDOWNEXTRA_VERSION', "1.2.5" ); # Sun 8 Jan 2012
+
+
+#
+# Global default settings:
+#
+
+# Change to ">" for HTML output
+@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />");
+
+# Define the width of a tab for code blocks.
+@define( 'MARKDOWN_TAB_WIDTH', 4 );
+
+# Optional title attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_TITLE', "" );
+@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" );
+
+# Optional class attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_CLASS', "" );
+@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" );
+
+
+#
+# WordPress settings:
+#
+
+# Change to false to remove Markdown from posts and/or comments.
+@define( 'MARKDOWN_WP_POSTS', true );
+@define( 'MARKDOWN_WP_COMMENTS', true );
+
+
+
+### Standard Function Interface ###
+
+@define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' );
+
+function Markdown($text) {
+#
+# Initialize the parser and return the result of its transform method.
+#
+ # Setup static parser variable.
+ static $parser;
+ if (!isset($parser)) {
+ $parser_class = MARKDOWN_PARSER_CLASS;
+ $parser = new $parser_class;
+ }
+
+ # Transform text using parser.
+ return $parser->transform($text);
+}
+
+
+### WordPress Plugin Interface ###
+
+/*
+Plugin Name: Markdown Extra
+Plugin URI: http://michelf.com/projects/php-markdown/
+Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...
+Version: 1.2.5
+Author: Michel Fortin
+Author URI: http://michelf.com/
+*/
+
+if (isset($wp_version)) {
+ # More details about how it works here:
+ #
+
+ # Post content and excerpts
+ # - Remove WordPress paragraph generator.
+ # - Run Markdown on excerpt, then remove all tags.
+ # - Add paragraph tag around the excerpt, but remove it for the excerpt rss.
+ if (MARKDOWN_WP_POSTS) {
+ remove_filter('the_content', 'wpautop');
+ remove_filter('the_content_rss', 'wpautop');
+ remove_filter('the_excerpt', 'wpautop');
+ add_filter('the_content', 'mdwp_MarkdownPost', 6);
+ add_filter('the_content_rss', 'mdwp_MarkdownPost', 6);
+ add_filter('get_the_excerpt', 'mdwp_MarkdownPost', 6);
+ add_filter('get_the_excerpt', 'trim', 7);
+ add_filter('the_excerpt', 'mdwp_add_p');
+ add_filter('the_excerpt_rss', 'mdwp_strip_p');
+
+ remove_filter('content_save_pre', 'balanceTags', 50);
+ remove_filter('excerpt_save_pre', 'balanceTags', 50);
+ add_filter('the_content', 'balanceTags', 50);
+ add_filter('get_the_excerpt', 'balanceTags', 9);
+ }
+
+ # Add a footnote id prefix to posts when inside a loop.
+ function mdwp_MarkdownPost($text) {
+ static $parser;
+ if (!$parser) {
+ $parser_class = MARKDOWN_PARSER_CLASS;
+ $parser = new $parser_class;
+ }
+ if (is_single() || is_page() || is_feed()) {
+ $parser->fn_id_prefix = "";
+ } else {
+ $parser->fn_id_prefix = get_the_ID() . ".";
+ }
+ return $parser->transform($text);
+ }
+
+ # Comments
+ # - Remove WordPress paragraph generator.
+ # - Remove WordPress auto-link generator.
+ # - Scramble important tags before passing them to the kses filter.
+ # - Run Markdown on excerpt then remove paragraph tags.
+ if (MARKDOWN_WP_COMMENTS) {
+ remove_filter('comment_text', 'wpautop', 30);
+ remove_filter('comment_text', 'make_clickable');
+ add_filter('pre_comment_content', 'Markdown', 6);
+ add_filter('pre_comment_content', 'mdwp_hide_tags', 8);
+ add_filter('pre_comment_content', 'mdwp_show_tags', 12);
+ add_filter('get_comment_text', 'Markdown', 6);
+ add_filter('get_comment_excerpt', 'Markdown', 6);
+ add_filter('get_comment_excerpt', 'mdwp_strip_p', 7);
+
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ $mdwp_hidden_tags = explode(' ',
+ '
", $text);
+ }
+ return $text;
+ }
+
+ function mdwp_strip_p($t) { return preg_replace('{?p>}i', '', $t); }
+
+ function mdwp_hide_tags($text) {
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text);
+ }
+ function mdwp_show_tags($text) {
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text);
+ }
+}
+
+
+### bBlog Plugin Info ###
+
+function identify_modifier_markdown() {
+ return array(
+ 'name' => 'markdown',
+ 'type' => 'modifier',
+ 'nicename' => 'PHP Markdown Extra',
+ 'description' => 'A text-to-HTML conversion tool for web writers',
+ 'authors' => 'Michel Fortin and John Gruber',
+ 'licence' => 'GPL',
+ 'version' => MARKDOWNEXTRA_VERSION,
+ 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...',
+ );
+}
+
+
+### Smarty Modifier Interface ###
+
+function smarty_modifier_markdown($text) {
+ return Markdown($text);
+}
+
+
+### Textile Compatibility Mode ###
+
+# Rename this file to "classTextile.php" and it can replace Textile everywhere.
+
+if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) {
+ # Try to include PHP SmartyPants. Should be in the same directory.
+ @include_once 'smartypants.php';
+ # Fake Textile class. It calls Markdown instead.
+ class Textile {
+ function TextileThis($text, $lite='', $encode='') {
+ if ($lite == '' && $encode == '') $text = Markdown($text);
+ if (function_exists('SmartyPants')) $text = SmartyPants($text);
+ return $text;
+ }
+ # Fake restricted version: restrictions are not supported for now.
+ function TextileRestricted($text, $lite='', $noimage='') {
+ return $this->TextileThis($text, $lite);
+ }
+ # Workaround to ensure compatibility with TextPattern 4.0.3.
+ function blockLite($text) { return $text; }
+ }
+}
+
+
+
+#
+# Markdown Parser Class
+#
+
+class Markdown_Parser {
+
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ var $nested_brackets_depth = 6;
+ var $nested_brackets_re;
+
+ var $nested_url_parenthesis_depth = 4;
+ var $nested_url_parenthesis_re;
+
+ # Table of hash values for escaped characters:
+ var $escape_chars = '\`*_{}[]()>#+-.!';
+ var $escape_chars_re;
+
+ # Change to ">" for HTML output.
+ var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX;
+ var $tab_width = MARKDOWN_TAB_WIDTH;
+
+ # Change to `true` to disallow markup or entities.
+ var $no_markup = false;
+ var $no_entities = false;
+
+ # Predefined urls and titles for reference links and images.
+ var $predef_urls = array();
+ var $predef_titles = array();
+
+
+ function Markdown_Parser() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+ $this->prepareItalicsAndBold();
+
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ $this->nested_url_parenthesis_re =
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+ $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
+
+
+ # Internal hashes used during transformation.
+ var $urls = array();
+ var $titles = array();
+ var $html_hashes = array();
+
+ # Status flag to avoid invalid nesting.
+ var $in_anchor = false;
+
+
+ function setup() {
+ #
+ # Called before the transformation process starts to setup parser
+ # states.
+ #
+ # Clear global hashes.
+ $this->urls = $this->predef_urls;
+ $this->titles = $this->predef_titles;
+ $this->html_hashes = array();
+
+ $in_anchor = false;
+ }
+
+ function teardown() {
+ #
+ # Called after the transformation process to clear any variable
+ # which may be taking up memory unnecessarly.
+ #
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_hashes = array();
+ }
+
+
+ function transform($text) {
+ #
+ # Main function. Performs some preprocessing on the input text
+ # and pass it through the document gamut.
+ #
+ $this->setup();
+
+ # Remove UTF-8 BOM and marker character in input, if present.
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = preg_replace('{\r\n?}', "\n", $text);
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = $this->detab($text);
+
+ # Turn block-level HTML blocks into hash entries
+ $text = $this->hashHTMLBlocks($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ ]*\n+/ .
+ $text = preg_replace('/^[ ]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ $this->teardown();
+
+ return $text . "\n";
+ }
+
+ var $document_gamut = array(
+ # Strip link definitions, store in hashes.
+ "stripLinkDefinitions" => 20,
+
+ "runBasicBlockGamut" => 30,
+ );
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ (?:
+ <(.+?)> # url = $2
+ |
+ (\S+?) # url = $3
+ )
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $4
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
+ $this->urls[$link_id] = $url;
+ $this->titles[$link_id] =& $matches[4];
+ return ''; # String that will replace the block
+ }
+
+
+ function hashHTMLBlocks($text) {
+ if ($this->no_markup) return $text;
+
+ $less_than_tab = $this->tab_width - 1;
+
+ # Hashify HTML blocks:
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap
s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded:
+ #
+ # * List "a" is made of tags which can be both inline or block-level.
+ # These will be treated block-level when the start tag is alone on
+ # its line, otherwise they're not matched here and will be taken as
+ # inline later.
+ # * List "b" is made of tags which are always block-level;
+ #
+ $block_tags_a_re = 'ins|del';
+ $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?>
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ \2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+ $content2 = str_replace('\2', '\3', $content);
+
+ # First, look for nested blocks, e.g.:
+ #
+ #
+ # tags for inner block must be indented.
+ #
+ #
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `
` and stop at the first `
`.
+ $text = preg_replace_callback('{(?>
+ (?>
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+
+ # Match from `\n` to `\n`, handling nested tags
+ # in between.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_b_re.')# start tag = $2
+ '.$attr.'> # attributes followed by > and \n
+ '.$content.' # content, support nesting
+ \2> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_a_re.')# start tag = $3
+ '.$attr.'>[ ]*\n # attributes followed by >
+ '.$content2.' # content, support nesting
+ \3> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for . It was easier to make a special
+ # case than to make the other regex more complicated.
+
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ '.$attr.' # attributes
+ /?> # the matching end tag
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # Special case for standalone HTML comments:
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # PHP and ASP-style processor instructions ( and <%)
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )}Sxmi',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_callback($matches) {
+ $text = $matches[1];
+ $key = $this->hashBlock($text);
+ return "\n\n$key\n\n";
+ }
+
+
+ function hashPart($text, $boundary = 'X') {
+ #
+ # Called whenever a tag must be hashed when a function insert an atomic
+ # element in the text stream. Passing $text to through this function gives
+ # a unique text-token which will be reverted back when calling unhash.
+ #
+ # The $boundary argument specify what character should be used to surround
+ # the token. By convension, "B" is used for block elements that needs not
+ # to be wrapped into paragraph tags at the end, ":" is used for elements
+ # that are word separators and "X" is used in the general case.
+ #
+ # Swap back any tag hash found in $text so we do not have to `unhash`
+ # multiple times at the end.
+ $text = $this->unhash($text);
+
+ # Then hash the block.
+ static $i = 0;
+ $key = "$boundary\x1A" . ++$i . $boundary;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the tag.
+ }
+
+
+ function hashBlock($text) {
+ #
+ # Shortcut function for hashPart with block-level boundaries.
+ #
+ return $this->hashPart($text, 'B');
+ }
+
+
+ var $block_gamut = array(
+ #
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ "doHeaders" => 10,
+ "doHorizontalRules" => 20,
+
+ "doLists" => 40,
+ "doCodeBlocks" => 50,
+ "doBlockQuotes" => 60,
+ );
+
+ function runBlockGamut($text) {
+ #
+ # Run block gamut tranformations.
+ #
+ # We need to escape raw HTML in Markdown source before doing anything
+ # else. This need to be done for each block, and not only at the
+ # begining in the Markdown function since hashed blocks can be part of
+ # list items and could have been indented. Indented blocks would have
+ # been seen as a code block in a previous pass of hashHTMLBlocks.
+ $text = $this->hashHTMLBlocks($text);
+
+ return $this->runBasicBlockGamut($text);
+ }
+
+ function runBasicBlockGamut($text) {
+ #
+ # Run block gamut tranformations, without hashing HTML blocks. This is
+ # useful when HTML blocks are known to be already hashed, like in the first
+ # whole-document pass.
+ #
+ foreach ($this->block_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ # Finally form paragraph and restore hashed blocks.
+ $text = $this->formParagraphs($text);
+
+ return $text;
+ }
+
+
+ function doHorizontalRules($text) {
+ # Do Horizontal Rules:
+ return preg_replace(
+ '{
+ ^[ ]{0,3} # Leading space
+ ([-*_]) # $1: First marker
+ (?> # Repeated marker group
+ [ ]{0,2} # Zero, one, or two spaces.
+ \1 # Marker character
+ ){2,} # Group repeated at least twice
+ [ ]* # Tailing spaces
+ $ # End of line.
+ }mx',
+ "\n".$this->hashBlock("empty_element_suffix")."\n",
+ $text);
+ }
+
+
+ var $span_gamut = array(
+ #
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ # Process character escapes, code spans, and inline HTML
+ # in one shot.
+ "parseSpan" => -30,
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ "doImages" => 10,
+ "doAnchors" => 20,
+
+ # Make links out of things like ``
+ # Must come after doAnchors, because you can use < and >
+ # delimiters in inline links like [this]().
+ "doAutoLinks" => 30,
+ "encodeAmpsAndAngles" => 40,
+
+ "doItalicsAndBold" => 50,
+ "doHardBreaks" => 60,
+ );
+
+ function runSpanGamut($text) {
+ #
+ # Run span gamut tranformations.
+ #
+ foreach ($this->span_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ return $text;
+ }
+
+
+ function doHardBreaks($text) {
+ # Do hard breaks:
+ return preg_replace_callback('/ {2,}\n/',
+ array(&$this, '_doHardBreaks_callback'), $text);
+ }
+ function _doHardBreaks_callback($matches) {
+ return $this->hashPart(" empty_element_suffix\n");
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML tags.
+ #
+ if ($this->in_anchor) return $text;
+ $this->in_anchor = true;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(.+?)> # href = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # href = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # Title = $7
+ \6 # matching quote
+ [ \n]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doAnchors_inline_callback'), $text);
+
+ #
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link text][1]
+ # or [link text](/foo)
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ $this->in_anchor = false;
+ return $text;
+ }
+ function _doAnchors_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $matches[2];
+ $link_id =& $matches[3];
+
+ if ($link_id == "") {
+ # for shortcut links like [this][] or [this].
+ $link_id = $link_text;
+ }
+
+ # lower-case and turn embedded newlines into spaces
+ $link_id = strtolower($link_id);
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAttribute($url);
+
+ $result = "titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+ $result = $this->hashPart($result);
+ }
+ else {
+ $result = $whole_match;
+ }
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+
+ return $this->hashPart($result);
+ }
+
+
+ function doImages($text) {
+ #
+ # Turn Markdown image shortcuts into tags.
+ #
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
+ #
+ # Next, handle inline images: 
+ # Don't forget: encode * and _
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(\S*)> # src url = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # src url = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ \n]*
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doImages_inline_callback'), $text);
+
+ return $text;
+ }
+ function _doImages_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $link_id = strtolower($matches[3]);
+
+ if ($link_id == "") {
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
+ }
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->encodeAttribute($this->urls[$link_id]);
+ $result = "titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashPart($result);
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ return $result;
+ }
+ function _doImages_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ # Terrible hack to check we haven't found an empty list item.
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+ return $matches[0];
+
+ $level = $matches[2]{0} == '=' ? 1 : 2;
+ $block = "".$this->runSpanGamut($matches[1])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $block = "".$this->runSpanGamut($matches[2])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doLists($text) {
+ #
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $markers_relist = array(
+ $marker_ul_re => $marker_ol_re,
+ $marker_ol_re => $marker_ul_re,
+ );
+
+ foreach ($markers_relist as $marker_re => $other_marker_re) {
+ # Re-usable pattern to match any entirel ul or ol list:
+ $whole_list_re = '
+ ( # $1 = whole list
+ ( # $2
+ ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces
+ ('.$marker_re.') # $4 = first list item marker
+ [ ]+
+ )
+ (?s:.+?)
+ ( # $5
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ ]*
+ '.$marker_re.'[ ]+
+ )
+ |
+ (?= # Lookahead for another kind of list
+ \n
+ \3 # Must have the same indentation
+ '.$other_marker_re.'[ ]+
+ )
+ )
+ )
+ '; // mx
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _ProcessListItems().
+
+ if ($this->list_level) {
+ $text = preg_replace_callback('{
+ ^
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ else {
+ $text = preg_replace_callback('{
+ (?:(?<=\n)\n|\A\n?) # Must eat the newline
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ }
+
+ return $text;
+ }
+ function _doLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $list = $matches[1];
+ $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+
+ $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+ $list .= "\n";
+ $result = $this->processListItems($list, $marker_any_re);
+
+ $result = $this->hashBlock("<$list_type>\n" . $result . "$list_type>");
+ return "\n". $result ."\n\n";
+ }
+
+ var $list_level = 0;
+
+ function processListItems($list_str, $marker_any_re) {
+ #
+ # Process the contents of a single ordered or unordered list, splitting it
+ # into individual list items.
+ #
+ # The $this->list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $this->list_level++;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ $list_str = preg_replace_callback('{
+ (\n)? # leading line = $1
+ (^[ ]*) # leading whitespace = $2
+ ('.$marker_any_re.' # list marker and space = $3
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
+ )
+ ((?s:.*?)) # list item text = $4
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
+ (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+ }xm',
+ array(&$this, '_processListItems_callback'), $list_str);
+
+ $this->list_level--;
+ return $list_str;
+ }
+ function _processListItems_callback($matches) {
+ $item = $matches[4];
+ $leading_line =& $matches[1];
+ $leading_space =& $matches[2];
+ $marker_space = $matches[3];
+ $tailing_blank_line =& $matches[5];
+
+ if ($leading_line || $tailing_blank_line ||
+ preg_match('/\n{2,}/', $item))
+ {
+ # Replace marker with the appropriate whitespace indentation
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+ $item = $this->runBlockGamut($this->outdent($item)."\n");
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = $this->doLists($this->outdent($item));
+ $item = preg_replace('/\n+$/', '', $item);
+ $item = $this->runSpanGamut($item);
+ }
+
+ return "
" . $item . "
\n";
+ }
+
+
+ function doCodeBlocks($text) {
+ #
+ # Process Markdown `
` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?>
+ [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }xm',
+ array(&$this, '_doCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ function _doCodeBlocks_callback($matches) {
+ $codeblock = $matches[1];
+
+ $codeblock = $this->outdent($codeblock);
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+
+ # trim leading newlines and trailing newlines
+ $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+ $codeblock = "
$codeblock\n
";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+
+
+ function makeCodeSpan($code) {
+ #
+ # Create a code span markup for $code. Called from handleSpanToken.
+ #
+ $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+ return $this->hashPart("$code");
+ }
+
+
+ var $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) {
+ foreach ($this->strong_relist as $strong => $strong_re) {
+ # Construct list of allowed token expressions.
+ $token_relist = array();
+ if (isset($this->em_strong_relist["$em$strong"])) {
+ $token_relist[] = $this->em_strong_relist["$em$strong"];
+ }
+ $token_relist[] = $em_re;
+ $token_relist[] = $strong_re;
+
+ # Construct master expression from list.
+ $token_re = '{('. implode('|', $token_relist) .')}';
+ $this->em_strong_prepared_relist["$em$strong"] = $token_re;
+ }
+ }
+ }
+
+ function doItalicsAndBold($text) {
+ $token_stack = array('');
+ $text_stack = array('');
+ $em = '';
+ $strong = '';
+ $tree_char_em = false;
+
+ while (1) {
+ #
+ # Get prepared regular expression for seraching emphasis tokens
+ # in current context.
+ #
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+ #
+ # Each loop iteration search for the next emphasis token.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+ $text_stack[0] .= $parts[0];
+ $token =& $parts[1];
+ $text =& $parts[2];
+
+ if (empty($token)) {
+ # Reached end of text span: empty stack without emitting.
+ # any more emphasis.
+ while ($token_stack[0]) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ break;
+ }
+
+ $token_len = strlen($token);
+ if ($tree_char_em) {
+ # Reached closing marker while inside a three-char emphasis.
+ if ($token_len == 3) {
+ # Three-char closing marker, close em and strong.
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ $strong = '';
+ } else {
+ # Other closing marker: close one em or strong and
+ # change current token state to match the other
+ $token_stack[0] = str_repeat($token{0}, 3-$token_len);
+ $tag = $token_len == 2 ? "strong" : "em";
+ $span = $text_stack[0];
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span$tag>";
+ $text_stack[0] = $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ $tree_char_em = false;
+ } else if ($token_len == 3) {
+ if ($em) {
+ # Reached closing marker for both em and strong.
+ # Closing strong marker:
+ for ($i = 0; $i < 2; ++$i) {
+ $shifted_token = array_shift($token_stack);
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span$tag>";
+ $text_stack[0] .= $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ } else {
+ # Reached opening three-char emphasis marker. Push on token
+ # stack; will be handled by the special condition above.
+ $em = $token{0};
+ $strong = "$em$em";
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $tree_char_em = true;
+ }
+ } else if ($token_len == 2) {
+ if ($strong) {
+ # Unwind any dangling emphasis marker:
+ if (strlen($token_stack[0]) == 1) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ # Closing strong marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $strong = '';
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $strong = $token;
+ }
+ } else {
+ # Here $token_len == 1
+ if ($em) {
+ if (strlen($token_stack[0]) == 1) {
+ # Closing emphasis marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ } else {
+ $text_stack[0] .= $token;
+ }
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $em = $token;
+ }
+ }
+ }
+ return $text_stack[0];
+ }
+
+
+ function doBlockQuotes($text) {
+ $text = preg_replace_callback('/
+ ( # Wrap whole match in $1
+ (?>
+ ^[ ]*>[ ]? # ">" at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ /xm',
+ array(&$this, '_doBlockQuotes_callback'), $text);
+
+ return $text;
+ }
+ function _doBlockQuotes_callback($matches) {
+ $bq = $matches[1];
+ # trim one level of quoting - trim whitespace-only lines
+ $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+ $bq = $this->runBlockGamut($bq); # recurse
+
+ $bq = preg_replace('/^/m', " ", $bq);
+ # These leading spaces cause problem with
content,
+ # so we need to fix that:
+ $bq = preg_replace_callback('{(\s*
+ #
+ # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+ # With some optimizations by Milian Wolff.
+ #
+ $addr = "mailto:" . $addr;
+ $chars = preg_split('/(? $char) {
+ $ord = ord($char);
+ # Ignore non-ascii chars.
+ if ($ord < 128) {
+ $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
+ # roughly 10% raw, 45% hex, 45% dec
+ # '@' *must* be encoded. I insist.
+ if ($r > 90 && $char != '@') /* do nothing */;
+ else if ($r < 45) $chars[$key] = ''.dechex($ord).';';
+ else $chars[$key] = ''.$ord.';';
+ }
+ }
+
+ $addr = implode('', $chars);
+ $text = implode('', array_slice($chars, 7)); # text without `mailto:`
+ $addr = "$text";
+
+ return $addr;
+ }
+
+
+ function parseSpan($str) {
+ #
+ # Take the string $str and parse it into tokens, hashing embeded HTML,
+ # escaped characters and handling code spans.
+ #
+ $output = '';
+
+ $span_re = '{
+ (
+ \\\\'.$this->escape_chars_re.'
+ |
+ (?no_markup ? '' : '
+ |
+ # comment
+ |
+ <\?.*?\?> | <%.*?%> # processing instruction
+ |
+ <[/!$]?[-a-zA-Z0-9:_]+ # regular tags
+ (?>
+ \s
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+ )?
+ >
+ ').'
+ )
+ }xs';
+
+ while (1) {
+ #
+ # Each loop iteration seach for either the next tag, the next
+ # openning code span marker, or the next escaped character.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $output .= $parts[0];
+ }
+
+ # Check if we reach the end.
+ if (isset($parts[1])) {
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
+ $str = $parts[2];
+ }
+ else {
+ break;
+ }
+ }
+
+ return $output;
+ }
+
+
+ function handleSpanToken($token, &$str) {
+ #
+ # Handle $token provided by parseSpan by determining its nature and
+ # returning the corresponding value that should replace it.
+ #
+ switch ($token{0}) {
+ case "\\":
+ return $this->hashPart("". ord($token{1}). ";");
+ case "`":
+ # Search for end marker in remaining text.
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ $str, $matches))
+ {
+ $str = $matches[2];
+ $codespan = $this->makeCodeSpan($matches[1]);
+ return $this->hashPart($codespan);
+ }
+ return $token; // return as text since no ending marker found.
+ default:
+ return $this->hashPart($token);
+ }
+ }
+
+
+ function outdent($text) {
+ #
+ # Remove one level of line-leading tabs or spaces
+ #
+ return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
+ }
+
+
+ # String length function for detab. `_initDetab` will create a function to
+ # hanlde UTF-8 if the default function does not exist.
+ var $utf8_strlen = 'mb_strlen';
+
+ function detab($text) {
+ #
+ # Replace tabs with the appropriate amount of space.
+ #
+ # For each line we separate the line in blocks delemited by
+ # tab characters. Then we reconstruct every line by adding the
+ # appropriate number of space between each blocks.
+
+ $text = preg_replace_callback('/^.*\t.*$/m',
+ array(&$this, '_detab_callback'), $text);
+
+ return $text;
+ }
+ function _detab_callback($matches) {
+ $line = $matches[0];
+ $strlen = $this->utf8_strlen; # strlen function for UTF-8.
+
+ # Split in blocks.
+ $blocks = explode("\t", $line);
+ # Add each blocks to the line.
+ $line = $blocks[0];
+ unset($blocks[0]); # Do not add first block twice.
+ foreach ($blocks as $block) {
+ # Calculate amount of space, insert spaces, insert block.
+ $amount = $this->tab_width -
+ $strlen($line, 'UTF-8') % $this->tab_width;
+ $line .= str_repeat(" ", $amount) . $block;
+ }
+ return $line;
+ }
+ function _initDetab() {
+ #
+ # Check for the availability of the function in the `utf8_strlen` property
+ # (initially `mb_strlen`). If the function is not available, create a
+ # function that will loosely count the number of UTF-8 characters with a
+ # regular expression.
+ #
+ if (function_exists($this->utf8_strlen)) return;
+ $this->utf8_strlen = create_function('$text', 'return preg_match_all(
+ "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
+ $text, $m);');
+ }
+
+
+ function unhash($text) {
+ #
+ # Swap back in all the tags hashed by _HashHTMLBlocks.
+ #
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+ array(&$this, '_unhash_callback'), $text);
+ }
+ function _unhash_callback($matches) {
+ return $this->html_hashes[$matches[0]];
+ }
+
+}
+
+
+#
+# Markdown Extra Parser Class
+#
+
+class MarkdownExtra_Parser extends Markdown_Parser {
+
+ # Prefix for footnote ids.
+ var $fn_id_prefix = "";
+
+ # Optional title attribute for footnote links and backlinks.
+ var $fn_link_title = MARKDOWN_FN_LINK_TITLE;
+ var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE;
+
+ # Optional class attribute for footnote links and backlinks.
+ var $fn_link_class = MARKDOWN_FN_LINK_CLASS;
+ var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS;
+
+ # Predefined abbreviations.
+ var $predef_abbr = array();
+
+
+ function MarkdownExtra_Parser() {
+ #
+ # Constructor function. Initialize the parser object.
+ #
+ # Add extra escapable characters before parent constructor
+ # initialize the table.
+ $this->escape_chars .= ':|';
+
+ # Insert extra document, block, and span transformations.
+ # Parent constructor will do the sorting.
+ $this->document_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "stripFootnotes" => 15,
+ "stripAbbreviations" => 25,
+ "appendFootnotes" => 50,
+ );
+ $this->block_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "doTables" => 15,
+ "doDefLists" => 45,
+ );
+ $this->span_gamut += array(
+ "doFootnotes" => 5,
+ "doAbbreviations" => 70,
+ );
+
+ parent::Markdown_Parser();
+ }
+
+
+ # Extra variables used during extra transformations.
+ var $footnotes = array();
+ var $footnotes_ordered = array();
+ var $abbr_desciptions = array();
+ var $abbr_word_re = '';
+
+ # Give the current footnote number.
+ var $footnote_counter = 1;
+
+
+ function setup() {
+ #
+ # Setting up Extra-specific variables.
+ #
+ parent::setup();
+
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+ $this->footnote_counter = 1;
+
+ foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ }
+ }
+
+ function teardown() {
+ #
+ # Clearing Extra-specific variables.
+ #
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+
+ parent::teardown();
+ }
+
+
+ ### HTML Block Parser ###
+
+ # Tags that are always treated as block tags:
+ var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend';
+
+ # Tags treated as block tags only if the opening tag is alone on it's line:
+ var $context_block_tags_re = 'script|noscript|math|ins|del';
+
+ # Tags where markdown="1" default to span mode:
+ var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
+
+ # Tags which must not have their contents modified, no matter where
+ # they appear:
+ var $clean_tags_re = 'script|math';
+
+ # Tags that do not need to be closed.
+ var $auto_close_tags_re = 'hr|img';
+
+
+ function hashHTMLBlocks($text) {
+ #
+ # Hashify HTML Blocks and "clean tags".
+ #
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap
s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded.
+ #
+ # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
+ # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
+ # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+ # These two functions are calling each other. It's recursive!
+ #
+ #
+ # Call the HTML-in-Markdown hasher.
+ #
+ list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
+ $enclosing_tag_re = '', $span = false)
+ {
+ #
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+ #
+ # * $indent is the number of space to be ignored when checking for code
+ # blocks. This is important because if we don't take the indent into
+ # account, something like this (which looks right) won't work as expected:
+ #
+ #
+ #
+ # Hello World. <-- Is this a Markdown code block or text?
+ #
<-- Is this a Markdown code block or a real tag?
+ #
+ #
+ # If you don't like this, just don't indent the tag on which
+ # you apply the markdown="1" attribute.
+ #
+ # * If $enclosing_tag_re is not empty, stops at the first unmatched closing
+ # tag with that name. Nested tags supported.
+ #
+ # * If $span is true, text inside must treated as span. So any double
+ # newline will be replaced by a single newline so that it does not create
+ # paragraphs.
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to check for the presense of newlines around a block tag.
+ $newline_before_re = '/(?:^\n?|\n\n)*$/';
+ $newline_after_re =
+ '{
+ ^ # Start of text following the tag.
+ (?>[ ]*)? # Optional comment.
+ [ ]*\n # Must be followed by newline.
+ }xs';
+
+ # Regex to match any tag.
+ $block_tag_re =
+ '{
+ ( # $2: Capture hole tag.
+ ? # Any opening or closing tag.
+ (?> # Tag name.
+ '.$this->block_tags_re.' |
+ '.$this->context_block_tags_re.' |
+ '.$this->clean_tags_re.' |
+ (?!\s)'.$enclosing_tag_re.'
+ )
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ |
+ # Code span marker
+ `+
+ '. ( !$span ? ' # If not in span.
+ |
+ # Indented code block
+ (?: ^[ ]*\n | ^ | \n[ ]*\n )
+ [ ]{'.($indent+4).'}[^\n]* \n
+ (?>
+ (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
+ )*
+ |
+ # Fenced code block marker
+ (?> ^ | \n )
+ [ ]{0,'.($indent).'}~~~+[ ]*\n
+ ' : '' ). ' # End (if not is span).
+ )
+ }xs';
+
+
+ $depth = 0; # Current depth inside the tag tree.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Loop through every tag until we find the closing tag of the parent
+ # or loop until reaching the end of text if no parent tag specified.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($block_tag_re, $text, 2,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ # If in Markdown span mode, add a empty-string span-level hash
+ # after each newline to prevent triggering any block element.
+ if ($span) {
+ $void = $this->hashPart("", ':');
+ $newline = "$void\n";
+ $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
+ }
+
+ $parsed .= $parts[0]; # Text before current tag.
+
+ # If end of $text has been reached. Stop loop.
+ if (count($parts) < 3) {
+ $text = "";
+ break;
+ }
+
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+ $tag_re = preg_quote($tag); # For use in a regular expression.
+
+ #
+ # Check for: Code span marker
+ #
+ if ($tag{0} == "`") {
+ # Find corresponding end marker.
+ $tag_re = preg_quote($tag);
+ if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)+?[ ]{0,'.($indent).'}'.$tag_re.'[ ]*\n}', $text,
+ $matches))
+ {
+ # End marker found: pass text unchanged until marker.
+ $parsed .= $tag . $matches[0];
+ $text = substr($text, strlen($matches[0]));
+ }
+ else {
+ # No end marker: just skip it.
+ $parsed .= $tag;
+ }
+ }
+ #
+ # Check for: Indented code block.
+ #
+ else if ($tag{0} == "\n" || $tag{0} == " ") {
+ # Indented code block: pass it unchanged, will be handled
+ # later.
+ $parsed .= $tag;
+ }
+ #
+ # Check for: Opening Block level tag or
+ # Opening Context Block tag (like ins and del)
+ # used as a block tag (tag is alone on it's line).
+ #
+ else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
+ ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
+ preg_match($newline_before_re, $parsed) &&
+ preg_match($newline_after_re, $text) )
+ )
+ {
+ # Need to parse tag and following text using the HTML parser.
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
+
+ # Make sure it stays outside of any paragraph by adding newlines.
+ $parsed .= "\n\n$block_text\n\n";
+ }
+ #
+ # Check for: Clean tag (like script, math)
+ # HTML Comments, processing instructions.
+ #
+ else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Need to parse tag and following text using the HTML parser.
+ # (don't check for markdown attribute)
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
+
+ $parsed .= $block_text;
+ }
+ #
+ # Check for: Tag with same name as enclosing tag.
+ #
+ else if ($enclosing_tag_re !== '' &&
+ # Same name as enclosing tag.
+ preg_match('{^?(?:'.$enclosing_tag_re.')\b}', $tag))
+ {
+ #
+ # Increase/decrease nested tag count.
+ #
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+
+ if ($depth < 0) {
+ #
+ # Going out of parent element. Clean up and break so we
+ # return to the calling function.
+ #
+ $text = $tag . $text;
+ break;
+ }
+
+ $parsed .= $tag;
+ }
+ else {
+ $parsed .= $tag;
+ }
+ } while ($depth >= 0);
+
+ return array($parsed, $text);
+ }
+ function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
+ #
+ # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
+ #
+ # * Calls $hash_method to convert any blocks.
+ # * Stops when the first opening tag closes.
+ # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
+ # (it is not inside clean tags)
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to match `markdown` attribute inside of a tag.
+ $markdown_attr_re = '
+ {
+ \s* # Eat whitespace before the `markdown` attribute
+ markdown
+ \s*=\s*
+ (?>
+ (["\']) # $1: quote delimiter
+ (.*?) # $2: attribute value
+ \1 # matching delimiter
+ |
+ ([^\s>]*) # $3: unquoted attribute value
+ )
+ () # $4: make $3 always defined (avoid warnings)
+ }xs';
+
+ # Regex to match any tag.
+ $tag_re = '{
+ ( # $2: Capture hole tag.
+ ? # Any opening or closing tag.
+ [\w:$]+ # Tag name.
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ )
+ }xs';
+
+ $original_text = $text; # Save original text in case of faliure.
+
+ $depth = 0; # Current depth inside the tag tree.
+ $block_text = ""; # Temporary text holder for current text.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Get the name of the starting tag.
+ # (This pattern makes $base_tag_name_re safe without quoting.)
+ #
+ if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
+ $base_tag_name_re = $matches[1];
+
+ #
+ # Loop through every tag until we find the corresponding closing tag.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ if (count($parts) < 3) {
+ #
+ # End of $text reached with unbalenced tag(s).
+ # In that case, we return original text unchanged and pass the
+ # first character as filtered to prevent an infinite loop in the
+ # parent function.
+ #
+ return array($original_text{0}, substr($original_text, 1));
+ }
+
+ $block_text .= $parts[0]; # Text before current tag.
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+
+ #
+ # Check for: Auto-close tag (like )
+ # Comments and Processing Instructions.
+ #
+ if (preg_match('{^?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Just add the tag to the block as if it was text.
+ $block_text .= $tag;
+ }
+ else {
+ #
+ # Increase/decrease nested tag count. Only do so if
+ # the tag's name match base tag's.
+ #
+ if (preg_match('{^?'.$base_tag_name_re.'\b}', $tag)) {
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+ }
+
+ #
+ # Check for `markdown="1"` attribute and handle it.
+ #
+ if ($md_attr &&
+ preg_match($markdown_attr_re, $tag, $attr_m) &&
+ preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
+ {
+ # Remove `markdown` attribute from opening tag.
+ $tag = preg_replace($markdown_attr_re, '', $tag);
+
+ # Check if text inside this tag must be parsed in span mode.
+ $this->mode = $attr_m[2] . $attr_m[3];
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+ preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
+
+ # Calculate indent before tag.
+ if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
+ $strlen = $this->utf8_strlen;
+ $indent = $strlen($matches[1], 'UTF-8');
+ } else {
+ $indent = 0;
+ }
+
+ # End preceding block with this tag.
+ $block_text .= $tag;
+ $parsed .= $this->$hash_method($block_text);
+
+ # Get enclosing tag name for the ParseMarkdown function.
+ # (This pattern makes $tag_name_re safe without quoting.)
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+ $tag_name_re = $matches[1];
+
+ # Parse the content using the HTML-in-Markdown parser.
+ list ($block_text, $text)
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
+ $tag_name_re, $span_mode);
+
+ # Outdent markdown text.
+ if ($indent > 0) {
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
+ $block_text);
+ }
+
+ # Append tag content to parsed text.
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
+ else $parsed .= "$block_text";
+
+ # Start over a new block.
+ $block_text = "";
+ }
+ else $block_text .= $tag;
+ }
+
+ } while ($depth > 0);
+
+ #
+ # Hash last block text that wasn't processed inside the loop.
+ #
+ $parsed .= $this->$hash_method($block_text);
+
+ return array($parsed, $text);
+ }
+
+
+ function hashClean($text) {
+ #
+ # Called whenever a tag must be hashed when a function insert a "clean" tag
+ # in $text, it pass through this function and is automaticaly escaped,
+ # blocking invalid nested overlap.
+ #
+ return $this->hashPart($text, 'C');
+ }
+
+
+ function doHeaders($text) {
+ #
+ # Redefined to add id attribute support.
+ #
+ # Setext-style headers:
+ # Header 1 {#header1}
+ # ========
+ #
+ # Header 2 {#header2}
+ # --------
+ #
+ $text = preg_replace_callback(
+ '{
+ (^.+?) # $1: Header text
+ (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # $2: Id attribute
+ [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1 {#header1}
+ # ## Header 2 {#header2}
+ # ## Header 2 with closing hashes ## {#header3}
+ # ...
+ # ###### Header 6 {#header2}
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute
+ [ ]*
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_attr($attr) {
+ if (empty($attr)) return "";
+ return " id=\"$attr\"";
+ }
+ function _doHeaders_callback_setext($matches) {
+ if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
+ return $matches[0];
+ $level = $matches[3]{0} == '=' ? 1 : 2;
+ $attr = $this->_doHeaders_attr($id =& $matches[2]);
+ $block = "".$this->runSpanGamut($matches[1])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $attr = $this->_doHeaders_attr($id =& $matches[3]);
+ $block = "".$this->runSpanGamut($matches[2])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doTables($text) {
+ #
+ # Form HTML tables.
+ #
+ $less_than_tab = $this->tab_width - 1;
+ #
+ # Find tables with leading pipe.
+ #
+ # | Header 1 | Header 2
+ # | -------- | --------
+ # | Cell 1 | Cell 2
+ # | Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] # Optional leading pipe (present)
+ (.+) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ [ ]* # Allowed whitespace.
+ [|] .* \n # Row content.
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_doTable_leadingPipe_callback'), $text);
+
+ #
+ # Find tables without leading pipe.
+ #
+ # Header 1 | Header 2
+ # -------- | --------
+ # Cell 1 | Cell 2
+ # Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ (\S.*[|].*) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ .* [|] .* \n # Row content
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_DoTable_callback'), $text);
+
+ return $text;
+ }
+ function _doTable_leadingPipe_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove leading pipe for each row.
+ $content = preg_replace('/^ *[|]/m', '', $content);
+
+ return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
+ }
+ function _doTable_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove any tailing pipes for each line.
+ $head = preg_replace('/[|] *$/m', '', $head);
+ $underline = preg_replace('/[|] *$/m', '', $underline);
+ $content = preg_replace('/[|] *$/m', '', $content);
+
+ # Reading alignement from header underline.
+ $separators = preg_split('/ *[|] */', $underline);
+ foreach ($separators as $n => $s) {
+ if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"';
+ else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"';
+ else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"';
+ else $attr[$n] = '';
+ }
+
+ # Parsing span elements, including code spans, character escapes,
+ # and inline HTML tags, so that pipes inside those gets ignored.
+ $head = $this->parseSpan($head);
+ $headers = preg_split('/ *[|] */', $head);
+ $col_count = count($headers);
+
+ # Write column headers.
+ $text = "
";
+
+ return $this->hashBlock($text) . "\n";
+ }
+
+
+ function doDefLists($text) {
+ #
+ # Form HTML definition lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable pattern to match any entire dl list:
+ $whole_list_re = '(?>
+ ( # $1 = whole list
+ ( # $2
+ [ ]{0,'.$less_than_tab.'}
+ ((?>.*\S.*\n)+) # $3 = defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?s:.+?)
+ ( # $4
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another term
+ [ ]{0,'.$less_than_tab.'}
+ (?: \S.*\n )+? # defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?! # Negative lookahead for another definition
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ )
+ )
+ )'; // mx
+
+ $text = preg_replace_callback('{
+ (?>\A\n?|(?<=\n\n))
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doDefLists_callback'), $text);
+
+ return $text;
+ }
+ function _doDefLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $list = $matches[1];
+
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $result = trim($this->processDefListItems($list));
+ $result = "
\n" . $result . "\n
";
+ return $this->hashBlock($result) . "\n\n";
+ }
+
+
+ function processDefListItems($list_str) {
+ #
+ # Process the contents of a single definition list, splitting it
+ # into individual term and definition list items.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ # Process definition terms.
+ $list_str = preg_replace_callback('{
+ (?>\A\n?|\n\n+) # leading line
+ ( # definition terms = $1
+ [ ]{0,'.$less_than_tab.'} # leading whitespace
+ (?![:][ ]|[ ]) # negative lookahead for a definition
+ # mark (colon) or more whitespace.
+ (?> \S.* \n)+? # actual term (not whitespace).
+ )
+ (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
+ # with a definition mark.
+ }xm',
+ array(&$this, '_processDefListItems_callback_dt'), $list_str);
+
+ # Process actual definitions.
+ $list_str = preg_replace_callback('{
+ \n(\n+)? # leading line = $1
+ ( # marker space = $2
+ [ ]{0,'.$less_than_tab.'} # whitespace before colon
+ [:][ ]+ # definition mark (colon)
+ )
+ ((?s:.+?)) # definition text = $3
+ (?= \n+ # stop at next definition mark,
+ (?: # next term or end of text
+ [ ]{0,'.$less_than_tab.'} [:][ ] |
+
";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+ function _doFencedCodeBlocks_newlines($matches) {
+ return str_repeat(" empty_element_suffix",
+ strlen($matches[0]));
+ }
+
+
+ #
+ # Redefining emphasis markers so that emphasis by underscore does not
+ # work in the middle of a word.
+ #
+ var $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap
tags and unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $value) {
+ $value = trim($this->runSpanGamut($value));
+
+ # Check if this should be enclosed in a paragraph.
+ # Clean tag hashes & block tag hashes are left alone.
+ $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
+
+ if ($is_p) {
+ $value = "
$value
";
+ }
+ $grafs[$key] = $value;
+ }
+
+ # Join grafs in one text, then unhash HTML tags.
+ $text = implode("\n\n", $grafs);
+
+ # Finish by removing any tag hashes still present in $text.
+ $text = $this->unhash($text);
+
+ return $text;
+ }
+
+
+ ### Footnotes
+
+ function stripFootnotes($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [^id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ ( # text = $2 (no blank lines allowed)
+ (?:
+ .+ # actual text
+ |
+ \n # newlines but
+ (?!\[\^.+?\]:\s)# negative lookahead for footnote marker.
+ (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
+ # by non-indented content
+ )*
+ )
+ }xm',
+ array(&$this, '_stripFootnotes_callback'),
+ $text);
+ return $text;
+ }
+ function _stripFootnotes_callback($matches) {
+ $note_id = $this->fn_id_prefix . $matches[1];
+ $this->footnotes[$note_id] = $this->outdent($matches[2]);
+ return ''; # String that will replace the block
+ }
+
+
+ function doFootnotes($text) {
+ #
+ # Replace footnote references in $text [^id] with a special text-token
+ # which will be replaced by the actual footnote marker in appendFootnotes.
+ #
+ if (!$this->in_anchor) {
+ $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
+ }
+ return $text;
+ }
+
+
+ function appendFootnotes($text) {
+ #
+ # Append footnote list to text.
+ #
+ $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
+ array(&$this, '_appendFootnotes_callback'), $text);
+
+ if (!empty($this->footnotes_ordered)) {
+ $text .= "\n\n";
+ $text .= "
";
+ }
+ return $text;
+ }
+ function _appendFootnotes_callback($matches) {
+ $node_id = $this->fn_id_prefix . $matches[1];
+
+ # Create footnote marker only if it has a corresponding footnote *and*
+ # the footnote hasn't been used by another marker.
+ if (isset($this->footnotes[$node_id])) {
+ # Transfert footnote content to the ordered list.
+ $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
+ unset($this->footnotes[$node_id]);
+
+ $num = $this->footnote_counter++;
+ $attr = " rel=\"footnote\"";
+ if ($this->fn_link_class != "") {
+ $class = $this->fn_link_class;
+ $class = $this->encodeAttribute($class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_link_title != "") {
+ $title = $this->fn_link_title;
+ $title = $this->encodeAttribute($title);
+ $attr .= " title=\"$title\"";
+ }
+
+ $attr = str_replace("%%", $num, $attr);
+ $node_id = $this->encodeAttribute($node_id);
+
+ return
+ "".
+ "$num".
+ "";
+ }
+
+ return "[^".$matches[1]."]";
+ }
+
+
+ ### Abbreviations ###
+
+ function stripAbbreviations($text) {
+ #
+ # Strips abbreviations from text, stores titles in hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [id]*: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1
+ (.*) # text = $2 (no blank lines allowed)
+ }xm',
+ array(&$this, '_stripAbbreviations_callback'),
+ $text);
+ return $text;
+ }
+ function _stripAbbreviations_callback($matches) {
+ $abbr_word = $matches[1];
+ $abbr_desc = $matches[2];
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ return ''; # String that will replace the block
+ }
+
+
+ function doAbbreviations($text) {
+ #
+ # Find defined abbreviations in text and wrap them in elements.
+ #
+ if ($this->abbr_word_re) {
+ // cannot use the /x modifier because abbr_word_re may
+ // contain significant spaces:
+ $text = preg_replace_callback('{'.
+ '(?abbr_word_re.')'.
+ '(?![\w\x1A])'.
+ '}',
+ array(&$this, '_doAbbreviations_callback'), $text);
+ }
+ return $text;
+ }
+ function _doAbbreviations_callback($matches) {
+ $abbr = $matches[0];
+ if (isset($this->abbr_desciptions[$abbr])) {
+ $desc = $this->abbr_desciptions[$abbr];
+ if (empty($desc)) {
+ return $this->hashPart("$abbr");
+ } else {
+ $desc = $this->encodeAttribute($desc);
+ return $this->hashPart("$abbr");
+ }
+ } else {
+ return $matches[0];
+ }
+ }
+
+}
+
+
+/*
+
+PHP Markdown Extra
+==================
+
+Description
+-----------
+
+This is a PHP port of the original Markdown formatter written in Perl
+by John Gruber. This special "Extra" version of PHP Markdown features
+further enhancements to the syntax for making additional constructs
+such as tables and definition list.
+
+Markdown is a text-to-HTML filter; it translates an easy-to-read /
+easy-to-write structured text format into HTML. Markdown's text format
+is most similar to that of plain text email, and supports features such
+as headers, *emphasis*, code blocks, blockquotes, and links.
+
+Markdown's syntax is designed not as a generic markup language, but
+specifically to serve as a front-end to (X)HTML. You can use span-level
+HTML tags anywhere in a Markdown document, and you can use block level
+HTML tags (like
and
as well).
+
+For more information about Markdown's syntax, see:
+
+
+
+
+Bugs
+----
+
+To file bug reports please send email to:
+
+
+
+Please include with your report: (1) the example input; (2) the output you
+expected; (3) the output Markdown actually produced.
+
+
+Version History
+---------------
+
+See the readme file for detailed release notes for this version.
+
+
+Copyright and License
+---------------------
+
+PHP Markdown & Extra
+Copyright (c) 2004-2009 Michel Fortin
+
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+
+*/
+?>
\ No newline at end of file
diff --git a/docs/lib/markdown/readme.md b/docs/lib/markdown/readme.md
new file mode 100755
index 0000000..e698576
--- /dev/null
+++ b/docs/lib/markdown/readme.md
@@ -0,0 +1,802 @@
+PHP Markdown Extra
+==================
+
+Version 1.2.5 - Sun 8 Jan 2012
+
+by Michel Fortin
+
+
+based on Markdown by John Gruber
+
+
+
+Introduction
+------------
+
+This is a special version of PHP Markdown with extra features. See
+ for details.
+
+Markdown is a text-to-HTML conversion tool for web writers. Markdown
+allows you to write using an easy-to-read, easy-to-write plain text
+format, then convert it to structurally valid XHTML (or HTML).
+
+"Markdown" is two things: a plain text markup syntax, and a software
+tool, written in Perl, that converts the plain text markup to HTML.
+PHP Markdown is a port to PHP of the original Markdown program by
+John Gruber.
+
+PHP Markdown can work as a plug-in for WordPress and bBlog, as a
+modifier for the Smarty templating engine, or as a remplacement for
+textile formatting in any software that support textile.
+
+Full documentation of Markdown's syntax is available on John's
+Markdown page:
+
+
+Installation and Requirement
+----------------------------
+
+PHP Markdown requires PHP version 4.0.5 or later.
+
+
+### WordPress ###
+
+PHP Markdown works with [WordPress][wp], version 1.2 or later.
+
+ [wp]: http://wordpress.org/
+
+1. To use PHP Markdown with WordPress, place the "makrdown.php" file
+ in the "plugins" folder. This folder is located inside
+ "wp-content" at the root of your site:
+
+ (site home)/wp-content/plugins/
+
+2. Activate the plugin with the administrative interface of
+ WordPress. In the "Plugins" section you will now find Markdown.
+ To activate the plugin, click on the "Activate" button on the
+ same line than Markdown. Your entries will now be formatted by
+ PHP Markdown.
+
+3. To post Markdown content, you'll first have to disable the
+ "visual" editor in the User section of WordPress.
+
+You can configure PHP Markdown to not apply to the comments on your
+WordPress weblog. See the "Configuration" section below.
+
+It is not possible at this time to apply a different set of
+filters to different entries. All your entries will be formated by
+PHP Markdown. This is a limitation of WordPress. If your old entries
+are written in HTML (as opposed to another formatting syntax, like
+Textile), they'll probably stay fine after installing Markdown.
+
+
+### bBlog ###
+
+PHP Markdown also works with [bBlog][bb].
+
+ [bb]: http://www.bblog.com/
+
+To use PHP Markdown with bBlog, rename "markdown.php" to
+"modifier.markdown.php" and place the file in the "bBlog_plugins"
+folder. This folder is located inside the "bblog" directory of
+your site, like this:
+
+ (site home)/bblog/bBlog_plugins/modifier.markdown.php
+
+Select "Markdown" as the "Entry Modifier" when you post a new
+entry. This setting will only apply to the entry you are editing.
+
+
+### Replacing Textile in TextPattern ###
+
+[TextPattern][tp] use [Textile][tx] to format your text. You can
+replace Textile by Markdown in TextPattern without having to change
+any code by using the *Texitle Compatibility Mode*. This may work
+with other software that expect Textile too.
+
+ [tx]: http://www.textism.com/tools/textile/
+ [tp]: http://www.textpattern.com/
+
+1. Rename the "markdown.php" file to "classTextile.php". This will
+ make PHP Markdown behave as if it was the actual Textile parser.
+
+2. Replace the "classTextile.php" file TextPattern installed in your
+ web directory. It can be found in the "lib" directory:
+
+ (site home)/textpattern/lib/
+
+Contrary to Textile, Markdown does not convert quotes to curly ones
+and does not convert multiple hyphens (`--` and `---`) into en- and
+em-dashes. If you use PHP Markdown in Textile Compatibility Mode, you
+can solve this problem by installing the "smartypants.php" file from
+[PHP SmartyPants][psp] beside the "classTextile.php" file. The Textile
+Compatibility Mode function will use SmartyPants automatically without
+further modification.
+
+ [psp]: http://michelf.com/projects/php-smartypants/
+
+
+### In Your Own Programs ###
+
+You can use PHP Markdown easily in your current PHP program. Simply
+include the file and then call the Markdown function on the text you
+want to convert:
+
+ include_once "markdown.php";
+ $my_html = Markdown($my_text);
+
+If you wish to use PHP Markdown with another text filter function
+built to parse HTML, you should filter the text *after* the Markdown
+function call. This is an example with [PHP SmartyPants][psp]:
+
+ $my_html = SmartyPants(Markdown($my_text));
+
+
+### With Smarty ###
+
+If your program use the [Smarty][sm] template engine, PHP Markdown
+can now be used as a modifier for your templates. Rename "markdown.php"
+to "modifier.markdown.php" and put it in your smarty plugins folder.
+
+ [sm]: http://smarty.php.net/
+
+If you are using MovableType 3.1 or later, the Smarty plugin folder is
+located at `(MT CGI root)/php/extlib/smarty/plugins`. This will allow
+Markdown to work on dynamic pages.
+
+
+### Updating Markdown in Other Programs ###
+
+Many web applications now ship with PHP Markdown, or have plugins to
+perform the conversion to HTML. You can update PHP Markdown -- or
+replace it with PHP Markdown Extra -- in many of these programs by
+swapping the old "markdown.php" file for the new one.
+
+Here is a short non-exhaustive list of some programs and where they
+hide the "markdown.php" file.
+
+| Program | Path to Markdown
+| ------- | ----------------
+| [Pivot][] | `(site home)/pivot/includes/markdown/`
+
+If you're unsure if you can do this with your application, ask the
+developer, or wait for the developer to update his application or
+plugin with the new version of PHP Markdown.
+
+ [Pivot]: http://pivotlog.net/
+
+
+Configuration
+-------------
+
+By default, PHP Markdown produces XHTML output for tags with empty
+elements. E.g.:
+
+
+
+Markdown can be configured to produce HTML-style tags; e.g.:
+
+
+
+To do this, you must edit the "MARKDOWN_EMPTY_ELEMENT_SUFFIX"
+definition below the "Global default settings" header at the start of
+the "markdown.php" file.
+
+
+### WordPress-Specific Settings ###
+
+By default, the Markdown plugin applies to both posts and comments on
+your WordPress weblog. To deactivate one or the other, edit the
+`MARKDOWN_WP_POSTS` or `MARKDOWN_WP_COMMENTS` definitions under the
+"WordPress settings" header at the start of the "markdown.php" file.
+
+
+Bugs
+----
+
+To file bug reports please send email to:
+
+
+Please include with your report: (1) the example input; (2) the output you
+expected; (3) the output PHP Markdown actually produced.
+
+
+Version History
+---------------
+
+1.0.1o (8 Jan 2012):
+
+* Silenced a new warning introduced around PHP 5.3 complaining about
+ POSIX characters classes not being implemented. PHP Markdown does not
+ use POSIX character classes, but it nevertheless trigged that warning.
+
+
+Extra 1.2.5 (8 Jan 2012):
+
+* Fixed an issue preventing fenced code blocks indented inside lists items
+ and elsewhere from being interpreted correctly.
+
+* Fixed an issue where HTML tags inside fenced code blocks were sometime
+ not encoded with entities.
+
+
+1.0.1n (10 Oct 2009):
+
+* Enabled reference-style shortcut links. Now you can write reference-style
+ links with less brakets:
+
+ This is [my website].
+
+ [my website]: http://example.com/
+
+ This was added in the 1.0.2 betas, but commented out in the 1.0.1 branch,
+ waiting for the feature to be officialized. [But half of the other Markdown
+ implementations are supporting this syntax][half], so it makes sense for
+ compatibility's sake to allow it in PHP Markdown too.
+
+ [half]: http://babelmark.bobtfish.net/?markdown=This+is+%5Bmy+website%5D.%0D%0A%09%09%0D%0A%5Bmy+website%5D%3A+http%3A%2F%2Fexample.com%2F%0D%0A&src=1&dest=2
+
+* Now accepting many valid email addresses in autolinks that were
+ previously rejected, such as:
+
+
+
+ <"abc@def"@example.com>
+ <"Fred Bloggs"@example.com>
+
+
+* Now accepting spaces in URLs for inline and reference-style links. Such
+ URLs need to be surrounded by angle brakets. For instance:
+
+ [link text]( "optional title")
+
+ [link text][ref]
+ [ref]: "optional title"
+
+ There is still a quirk which may prevent this from working correctly with
+ relative URLs in inline-style links however.
+
+* Fix for adjacent list of different kind where the second list could
+ end as a sublist of the first when not separated by an empty line.
+
+* Fixed a bug where inline-style links wouldn't be recognized when the link
+ definition contains a line break between the url and the title.
+
+* Fixed a bug where tags where the name contains an underscore aren't parsed
+ correctly.
+
+* Fixed some corner-cases mixing underscore-ephasis and asterisk-emphasis.
+
+
+Extra 1.2.4 (10 Oct 2009):
+
+* Fixed a problem where unterminated tags in indented code blocks could
+ prevent proper escaping of characaters in the code block.
+
+
+Extra 1.2.3 (31 Dec 2008):
+
+* In WordPress pages featuring more than one post, footnote id prefixes are
+ now automatically applied with the current post ID to avoid clashes
+ between footnotes belonging to different posts.
+
+* Fix for a bug introduced in Extra 1.2 where block-level HTML tags where
+ not detected correctly, thus the addition of erroneous `
` tags and
+ interpretation of their content as Markdown-formatted instead of
+ HTML-formatted.
+
+
+Extra 1.2.2 (21 Jun 2008):
+
+* Fixed a problem where abbreviation definitions, footnote
+ definitions and link references were stripped inside
+ fenced code blocks.
+
+* Fixed a bug where characters such as `"` in abbreviation
+ definitions weren't properly encoded to HTML entities.
+
+* Fixed a bug where double quotes `"` were not correctly encoded
+ as HTML entities when used inside a footnote reference id.
+
+
+1.0.1m (21 Jun 2008):
+
+* Lists can now have empty items.
+
+* Rewrote the emphasis and strong emphasis parser to fix some issues
+ with odly placed and overlong markers.
+
+
+Extra 1.2.1 (27 May 2008):
+
+* Fixed a problem where Markdown headers and horizontal rules were
+ transformed into their HTML equivalent inside fenced code blocks.
+
+
+Extra 1.2 (11 May 2008):
+
+* Added fenced code block syntax which don't require indentation
+ and can start and end with blank lines. A fenced code block
+ starts with a line of consecutive tilde (~) and ends on the
+ next line with the same number of consecutive tilde. Here's an
+ example:
+
+ ~~~~~~~~~~~~
+ Hello World!
+ ~~~~~~~~~~~~
+
+* Rewrote parts of the HTML block parser to better accomodate
+ fenced code blocks.
+
+* Footnotes may now be referenced from within another footnote.
+
+* Added programatically-settable parser property `predef_attr` for
+ predefined attribute definitions.
+
+* Fixed an issue where an indented code block preceded by a blank
+ line containing some other whitespace would confuse the HTML
+ block parser into creating an HTML block when it should have
+ been code.
+
+
+1.0.1l (11 May 2008):
+
+* Now removing the UTF-8 BOM at the start of a document, if present.
+
+* Now accepting capitalized URI schemes (such as HTTP:) in automatic
+ links, such as ``.
+
+* Fixed a problem where `
` was seen as a horizontal
+ rule instead of an automatic link.
+
+* Fixed an issue where some characters in Markdown-generated HTML
+ attributes weren't properly escaped with entities.
+
+* Fix for code blocks as first element of a list item. Previously,
+ this didn't create any code block for item 2:
+
+ * Item 1 (regular paragraph)
+
+ * Item 2 (code block)
+
+* A code block starting on the second line of a document wasn't seen
+ as a code block. This has been fixed.
+
+* Added programatically-settable parser properties `predef_urls` and
+ `predef_titles` for predefined URLs and titles for reference-style
+ links. To use this, your PHP code must call the parser this way:
+
+ $parser = new Markdwon_Parser;
+ $parser->predef_urls = array('linkref' => 'http://example.com');
+ $html = $parser->transform($text);
+
+ You can then use the URL as a normal link reference:
+
+ [my link][linkref]
+ [my link][linkRef]
+
+ Reference names in the parser properties *must* be lowercase.
+ Reference names in the Markdown source may have any case.
+
+* Added `setup` and `teardown` methods which can be used by subclassers
+ as hook points to arrange the state of some parser variables before and
+ after parsing.
+
+
+Extra 1.1.7 (26 Sep 2007):
+
+1.0.1k (26 Sep 2007):
+
+* Fixed a problem introduced in 1.0.1i where three or more identical
+ uppercase letters, as well as a few other symbols, would trigger
+ a horizontal line.
+
+
+Extra 1.1.6 (4 Sep 2007):
+
+1.0.1j (4 Sep 2007):
+
+* Fixed a problem introduced in 1.0.1i where the closing `code` and
+ `pre` tags at the end of a code block were appearing in the wrong
+ order.
+
+* Overriding configuration settings by defining constants from an
+ external before markdown.php is included is now possible without
+ producing a PHP warning.
+
+
+Extra 1.1.5 (31 Aug 2007):
+
+1.0.1i (31 Aug 2007):
+
+* Fixed a problem where an escaped backslash before a code span
+ would prevent the code span from being created. This should now
+ work as expected:
+
+ Litteral backslash: \\`code span`
+
+* Overall speed improvements, especially with long documents.
+
+
+Extra 1.1.4 (3 Aug 2007):
+
+1.0.1h (3 Aug 2007):
+
+* Added two properties (`no_markup` and `no_entities`) to the parser
+ allowing HTML tags and entities to be disabled.
+
+* Fix for a problem introduced in 1.0.1g where posting comments in
+ WordPress would trigger PHP warnings and cause some markup to be
+ incorrectly filtered by the kses filter in WordPress.
+
+
+Extra 1.1.3 (3 Jul 2007):
+
+* Fixed a performance problem when parsing some invalid HTML as an HTML
+ block which was resulting in too much recusion and a segmentation fault
+ for long documents.
+
+* The markdown="" attribute now accepts unquoted values.
+
+* Fixed an issue where underscore-emphasis didn't work when applied on the
+ first or the last word of an element having the markdown="1" or
+ markdown="span" attribute set unless there was some surrounding whitespace.
+ This didn't work:
+
+
_Hello_ _world_
+
+ Now it does produce emphasis as expected.
+
+* Fixed an issue preventing footnotes from working when the parser's
+ footnote id prefix variable (fn_id_prefix) is not empty.
+
+* Fixed a performance problem where the regular expression for strong
+ emphasis introduced in version 1.1 could sometime be long to process,
+ give slightly wrong results, and in some circumstances could remove
+ entirely the content for a whole paragraph.
+
+* Fixed an issue were abbreviations tags could be incorrectly added
+ inside URLs and title of links.
+
+* Placing footnote markers inside a link, resulting in two nested links, is
+ no longer allowed.
+
+
+1.0.1g (3 Jul 2007):
+
+* Fix for PHP 5 compiled without the mbstring module. Previous fix to
+ calculate the length of UTF-8 strings in `detab` when `mb_strlen` is
+ not available was only working with PHP 4.
+
+* Fixed a problem with WordPress 2.x where full-content posts in RSS feeds
+ were not processed correctly by Markdown.
+
+* Now supports URLs containing literal parentheses for inline links
+ and images, such as:
+
+ [WIMP](http://en.wikipedia.org/wiki/WIMP_(computing))
+
+ Such parentheses may be arbitrarily nested, but must be
+ balanced. Unbalenced parentheses are allowed however when the URL
+ when escaped or when the URL is enclosed in angle brakets `<>`.
+
+* Fixed a performance problem where the regular expression for strong
+ emphasis introduced in version 1.0.1d could sometime be long to process,
+ give slightly wrong results, and in some circumstances could remove
+ entirely the content for a whole paragraph.
+
+* Some change in version 1.0.1d made possible the incorrect nesting of
+ anchors within each other. This is now fixed.
+
+* Fixed a rare issue where certain MD5 hashes in the content could
+ be changed to their corresponding text. For instance, this:
+
+ The MD5 value for "+" is "26b17225b626fb9238849fd60eabdf60".
+
+ was incorrectly changed to this in previous versions of PHP Markdown:
+
+
The MD5 value for "+" is "+".
+
+* Now convert escaped characters to their numeric character
+ references equivalent.
+
+ This fix an integration issue with SmartyPants and backslash escapes.
+ Since Markdown and SmartyPants have some escapable characters in common,
+ it was sometime necessary to escape them twice. Previously, two
+ backslashes were sometime required to prevent Markdown from "eating" the
+ backslash before SmartyPants sees it:
+
+ Here are two hyphens: \\--
+
+ Now, only one backslash will do:
+
+ Here are two hyphens: \--
+
+
+Extra 1.1.2 (7 Feb 2007)
+
+* Fixed an issue where headers preceded too closely by a paragraph
+ (with no blank line separating them) where put inside the paragraph.
+
+* Added the missing TextileRestricted method that was added to regular
+ PHP Markdown since 1.0.1d but which I forgot to add to Extra.
+
+
+1.0.1f (7 Feb 2007):
+
+* Fixed an issue with WordPress where manually-entered excerpts, but
+ not the auto-generated ones, would contain nested paragraphs.
+
+* Fixed an issue introduced in 1.0.1d where headers and blockquotes
+ preceded too closely by a paragraph (not separated by a blank line)
+ where incorrectly put inside the paragraph.
+
+* Fixed an issue introduced in 1.0.1d in the tokenizeHTML method where
+ two consecutive code spans would be merged into one when together they
+ form a valid tag in a multiline paragraph.
+
+* Fixed an long-prevailing issue where blank lines in code blocks would
+ be doubled when the code block is in a list item.
+
+ This was due to the list processing functions relying on artificially
+ doubled blank lines to correctly determine when list items should
+ contain block-level content. The list item processing model was thus
+ changed to avoid the need for double blank lines.
+
+* Fixed an issue with `<% asp-style %>` instructions used as inline
+ content where the opening `<` was encoded as `<`.
+
+* Fixed a parse error occuring when PHP is configured to accept
+ ASP-style delimiters as boundaries for PHP scripts.
+
+* Fixed a bug introduced in 1.0.1d where underscores in automatic links
+ got swapped with emphasis tags.
+
+
+Extra 1.1.1 (28 Dec 2006)
+
+* Fixed a problem where whitespace at the end of the line of an atx-style
+ header would cause tailing `#` to appear as part of the header's content.
+ This was caused by a small error in the regex that handles the definition
+ for the id attribute in PHP Markdown Extra.
+
+* Fixed a problem where empty abbreviations definitions would eat the
+ following line as its definition.
+
+* Fixed an issue with calling the Markdown parser repetitivly with text
+ containing footnotes. The footnote hashes were not reinitialized properly.
+
+
+1.0.1e (28 Dec 2006)
+
+* Added support for internationalized domain names for email addresses in
+ automatic link. Improved the speed at which email addresses are converted
+ to entities. Thanks to Milian Wolff for his optimisations.
+
+* Made deterministic the conversion to entities of email addresses in
+ automatic links. This means that a given email address will always be
+ encoded the same way.
+
+* PHP Markdown will now use its own function to calculate the length of an
+ UTF-8 string in `detab` when `mb_strlen` is not available instead of
+ giving a fatal error.
+
+
+Extra 1.1 (1 Dec 2006)
+
+* Added a syntax for footnotes.
+
+* Added an experimental syntax to define abbreviations.
+
+
+1.0.1d (1 Dec 2006)
+
+* Fixed a bug where inline images always had an empty title attribute. The
+ title attribute is now present only when explicitly defined.
+
+* Link references definitions can now have an empty title, previously if the
+ title was defined but left empty the link definition was ignored. This can
+ be useful if you want an empty title attribute in images to hide the
+ tooltip in Internet Explorer.
+
+* Made `detab` aware of UTF-8 characters. UTF-8 multi-byte sequences are now
+ correctly mapped to one character instead of the number of bytes.
+
+* Fixed a small bug with WordPress where WordPress' default filter `wpautop`
+ was not properly deactivated on comment text, resulting in hard line breaks
+ where Markdown do not prescribes them.
+
+* Added a `TextileRestrited` method to the textile compatibility mode. There
+ is no restriction however, as Markdown does not have a restricted mode at
+ this point. This should make PHP Markdown work again in the latest
+ versions of TextPattern.
+
+* Converted PHP Markdown to a object-oriented design.
+
+* Changed span and block gamut methods so that they loop over a
+ customizable list of methods. This makes subclassing the parser a more
+ interesting option for creating syntax extensions.
+
+* Also added a "document" gamut loop which can be used to hook document-level
+ methods (like for striping link definitions).
+
+* Changed all methods which were inserting HTML code so that they now return
+ a hashed representation of the code. New methods `hashSpan` and `hashBlock`
+ are used to hash respectivly span- and block-level generated content. This
+ has a couple of significant effects:
+
+ 1. It prevents invalid nesting of Markdown-generated elements which
+ could occur occuring with constructs like `*something [link*][1]`.
+ 2. It prevents problems occuring with deeply nested lists on which
+ paragraphs were ill-formed.
+ 3. It removes the need to call `hashHTMLBlocks` twice during the the
+ block gamut.
+
+ Hashes are turned back to HTML prior output.
+
+* Made the block-level HTML parser smarter using a specially-crafted regular
+ expression capable of handling nested tags.
+
+* Solved backtick issues in tag attributes by rewriting the HTML tokenizer to
+ be aware of code spans. All these lines should work correctly now:
+
+ bar
+ bar
+ ``
+
+* Changed the parsing of HTML comments to match simply from ``
+ instead using of the more complicated SGML-style rule with paired `--`.
+ This is how most browsers parse comments and how XML defines them too.
+
+* `` has been added to the list of block-level elements and is now
+ treated as an HTML block instead of being wrapped within paragraph tags.
+
+* Now only trim trailing newlines from code blocks, instead of trimming
+ all trailing whitespace characters.
+
+* Fixed bug where this:
+
+ [text](http://m.com "title" )
+
+ wasn't working as expected, because the parser wasn't allowing for spaces
+ before the closing paren.
+
+* Filthy hack to support markdown='1' in div tags.
+
+* _DoAutoLinks() now supports the 'dict://' URL scheme.
+
+* PHP- and ASP-style processor instructions are now protected as
+ raw HTML blocks.
+
+ ... ?>
+ <% ... %>
+
+* Fix for escaped backticks still triggering code spans:
+
+ There are two raw backticks here: \` and here: \`, not a code span
+
+
+Extra 1.0 - 5 September 2005
+
+* Added support for setting the id attributes for headers like this:
+
+ Header 1 {#header1}
+ ========
+
+ ## Header 2 ## {#header2}
+
+ This only work only for headers for now.
+
+* Tables will now work correctly as the first element of a definition
+ list. For example, this input:
+
+ Term
+
+ : Header | Header
+ ------- | -------
+ Cell | Cell
+
+ used to produce no definition list and a table where the first
+ header was named ": Header". This is now fixed.
+
+* Fix for a problem where a paragraph following a table was not
+ placed between `
` tags.
+
+
+Extra 1.0b4 - 1 August 2005
+
+* Fixed some issues where whitespace around HTML blocks were trigging
+ empty paragraph tags.
+
+* Fixed an HTML block parsing issue that would cause a block element
+ following a code span or block with unmatched opening bracket to be
+ placed inside a paragraph.
+
+* Removed some PHP notices that could appear when parsing definition
+ lists and tables with PHP notice reporting flag set.
+
+
+Extra 1.0b3 - 29 July 2005
+
+* Definition lists now require a blank line before each term. Solves
+ an ambiguity where the last line of lazy-indented definitions could
+ be mistaken by PHP Markdown as a new term in the list.
+
+* Definition lists now support multiple terms per definition.
+
+* Some special tags were replaced in the output by their md5 hash
+ key. Things such as this now work as expected:
+
+ ## Header ##
+
+
+Extra 1.0b2 - 26 July 2005
+
+* Definition lists can now take two or more definitions for one term.
+ This should have been the case before, but a bug prevented this
+ from working right.
+
+* Fixed a problem where single column table with a pipe only at the
+ end where not parsed as table. Here is such a table:
+
+ | header
+ | ------
+ | cell
+
+* Fixed problems with empty cells in the first column of a table with
+ no leading pipe, like this one:
+
+ header | header
+ ------ | ------
+ | cell
+
+* Code spans containing pipes did not within a table. This is now
+ fixed by parsing code spans before splitting rows into cells.
+
+* Added the pipe character to the backlash escape character lists.
+
+Extra 1.0b1 (25 Jun 2005)
+
+* First public release of PHP Markdown Extra.
+
+
+Copyright and License
+---------------------
+
+PHP Markdown & Extra
+Copyright (c) 2004-2009 Michel Fortin
+
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2005 John Gruber
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the
+ distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
diff --git a/docs/nav.php b/docs/nav.php
new file mode 100644
index 0000000..e90e3b9
--- /dev/null
+++ b/docs/nav.php
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/styles/elements.less b/docs/styles/elements.less
new file mode 100755
index 0000000..ffda4f1
--- /dev/null
+++ b/docs/styles/elements.less
@@ -0,0 +1,142 @@
+/*---------------------------------------------------
+ LESS Elements 0.6
+ ---------------------------------------------------
+ A set of useful LESS mixins by Dmitry Fadeyev
+ Special thanks for mixin suggestions to:
+ Kris Van Herzeele,
+ Benoit Adam,
+ Portenart Emile-Victor,
+ Ryan Faerman
+
+ More info at: http://lesselements.com
+-----------------------------------------------------*/
+
+.gradient(@color: #F5F5F5, @start: #EEE, @stop: #FFF) {
+ background: @color;
+ background: -webkit-gradient(linear,
+ left bottom,
+ left top,
+ color-stop(0, @start),
+ color-stop(1, @stop));
+ background: -ms-linear-gradient(bottom,
+ @start,
+ @stop);
+ background: -moz-linear-gradient(center bottom,
+ @start 0%,
+ @stop 100%);
+}
+.bw-gradient(@color: #F5F5F5, @start: 0, @stop: 255) {
+ background: @color;
+ background: -webkit-gradient(linear,
+ left bottom,
+ left top,
+ color-stop(0, rgb(@start,@start,@start)),
+ color-stop(1, rgb(@stop,@stop,@stop)));
+ background: -ms-linear-gradient(bottom,
+ rgb(@start,@start,@start) 0%,
+ rgb(@start,@start,@start) 100%);
+ background: -moz-linear-gradient(center bottom,
+ rgb(@start,@start,@start) 0%,
+ rgb(@stop,@stop,@stop) 100%);
+}
+.bordered(@top-color: #EEE, @right-color: #EEE, @bottom-color: #EEE, @left-color: #EEE) {
+ border-top: solid 1px @top-color;
+ border-left: solid 1px @left-color;
+ border-right: solid 1px @right-color;
+ border-bottom: solid 1px @bottom-color;
+}
+.drop-shadow(@x-axis: 0, @y-axis: 1px, @blur: 2px, @alpha: 0.1) {
+ -webkit-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha);
+ -moz-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha);
+ box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha);
+}
+.rounded(@radius: 2px) {
+ -webkit-border-radius: @radius;
+ -moz-border-radius: @radius;
+ border-radius: @radius;
+ -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box;
+}
+.border-radius(@topright: 0, @bottomright: 0, @bottomleft: 0, @topleft: 0) {
+ -webkit-border-top-right-radius: @topright;
+ -webkit-border-bottom-right-radius: @bottomright;
+ -webkit-border-bottom-left-radius: @bottomleft;
+ -webkit-border-top-left-radius: @topleft;
+ -moz-border-radius-topright: @topright;
+ -moz-border-radius-bottomright: @bottomright;
+ -moz-border-radius-bottomleft: @bottomleft;
+ -moz-border-radius-topleft: @topleft;
+ border-top-right-radius: @topright;
+ border-bottom-right-radius: @bottomright;
+ border-bottom-left-radius: @bottomleft;
+ border-top-left-radius: @topleft;
+ -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box;
+}
+.opacity(@opacity: 0.5) {
+ -moz-opacity: @opacity;
+ -khtml-opacity: @opacity;
+ -webkit-opacity: @opacity;
+ opacity: @opacity;
+}
+.transition-duration(@duration: 0.2s) {
+ -moz-transition-duration: @duration;
+ -webkit-transition-duration: @duration;
+ transition-duration: @duration;
+}
+.rotation(@deg:5deg){
+ -webkit-transform: rotate(@deg);
+ -moz-transform: rotate(@deg);
+ transform: rotate(@deg);
+}
+.scale(@ratio:1.5){
+ -webkit-transform:scale(@ratio);
+ -moz-transform:scale(@ratio);
+ transform:scale(@ratio);
+}
+.transition(@duration:0.2s, @ease:ease-out, @prop:all) {
+ -webkit-transition: @prop @duration @ease;
+ -moz-transition: @prop @duration @ease;
+ transition: @prop @duration @ease;
+}
+.transition2(@d1:0.2s, @e1:ease-out, @p1:all,
+ @d2:0.2s, @e2:ease-out, @p2:all) {
+ -webkit-transition: @d1 @e1 @p1, @d2 @e2 @p2;
+ -moz-transition: @d1 @e1 @p1, @d2 @e2 @p2;
+ transition: @d1 @e1 @p1, @d2 @e2 @p2;
+}
+.inner-shadow(@horizontal:0, @vertical:1px, @blur:2px, @alpha: 0.4) {
+ -webkit-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha);
+ -moz-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha);
+ box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha);
+}
+.box-shadow(@arguments) {
+ -webkit-box-shadow: @arguments;
+ -moz-box-shadow: @arguments;
+ box-shadow: @arguments;
+}
+.columns(@colwidth: 250px, @colcount: 0, @colgap: 50px, @columnRuleColor: #EEE, @columnRuleStyle: solid, @columnRuleWidth: 1px) {
+ -moz-column-width: @colwidth;
+ -moz-column-count: @colcount;
+ -moz-column-gap: @colgap;
+ -moz-column-rule-color: @columnRuleColor;
+ -moz-column-rule-style: @columnRuleStyle;
+ -moz-column-rule-width: @columnRuleWidth;
+ -webkit-column-width: @colwidth;
+ -webkit-column-count: @colcount;
+ -webkit-column-gap: @colgap;
+ -webkit-column-rule-color: @columnRuleColor;
+ -webkit-column-rule-style: @columnRuleStyle;
+ -webkit-column-rule-width: @columnRuleWidth;
+ column-width: @colwidth;
+ column-count: @colcount;
+ column-gap: @colgap;
+ column-rule-color: @columnRuleColor;
+ column-rule-style: @columnRuleStyle;
+ column-rule-width: @columnRuleWidth;
+}
+.translate(@x:0, @y:0) {
+ -moz-transform: translate(@x, @y);
+ -webkit-transform: translate(@x, @y);
+ -o-transform: translate(@x, @y);
+ -ms-transform: translate(@x, @y);
+ transform: translate(@x, @y);
+}
diff --git a/docs/styles/presto.css b/docs/styles/presto.css
new file mode 100644
index 0000000..8828069
--- /dev/null
+++ b/docs/styles/presto.css
@@ -0,0 +1,20 @@
+body{font:115%/150% "freight-sans-pro","Helvetica Neau",Helvetica;}body header,body section,body footer{width:100%;position:relative;}
+body header>div,body section>div,body footer>div{position:relative;width:40em;margin:0 auto;}
+body>a{position:fixed;right:0;top:0;z-index:999;display:block;width:125px;height:125px;}
+body a,body a:visited,body a:hover{color:#963232;text-decoration:none;}
+body h1,body h2,body h4,body h5{margin:2em 0 0;}
+body h3{margin:1.75em 0 0;font-size:smaller;font-weight:400;text-transform:uppercase;}
+body code,body pre{font:10pt/100% Menlo,Monaco,"Andale Mono",monospace;}
+body pre{background-color:rgba(0, 0, 0, 0.1);padding:.75em 1em;-webkit-border-radius:0.5em;-moz-border-radius:0.5em;border-radius:0.5em;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;overflow:hidden;}
+body pre.highlight{border:0 !important;padding:.75em !important;width:97% !important;font:10pt/100% Menlo,Monaco,"Andale Mono",monospace !important;}
+body p{margin:.25em 0 1em;}
+body p>code{background-color:rgba(0, 0, 0, 0.1);padding:.35em .25em;margin:0 .25em 0 0;-webkit-border-radius:0.25em;-moz-border-radius:0.25em;border-radius:0.25em;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;}
+body header h1{margin:0 0 .5em;}body header h1 em{display:block;font-size:10pt;font-style:normal;font-weight:lighter;color:#333;}
+body header nav{position:absolute;right:0;top:.5em;width:auto;}body header nav a{display:block;float:left;width:5em;line-height:95%;margin-right:1em;padding-bottom:.25em;font-size:11pt;font-weight:lighter;text-align:center;text-transform:uppercase;border-bottom:4px solid transparent;}
+body header nav a:hover{border-color:#963232;}
+body footer div{border-top:1px solid rgba(0, 0, 0, 0.25);margin-top:3em;}body footer div h5{font-weight:200;}
+body details:nth-child(1){margin-top:2em;}
+body details{border:1px solid rgba(0, 0, 0, 0.25);padding:.25em .5em;margin:.25em .5em;}body details summary::-webkit-details-marker{content:'...';}
+body details summary{position:relative;font-weight:bold;outline:none;cursor:pointer;}body details summary var{position:absolute;right:0;top:0;width:3em;text-align:center;font-style:normal;font-weight:bold;background-color:rgba(0, 0, 0, 0.25);color:rgba(0, 0, 0, 0.5);}
+body details summary var.fail,body details summary var.off{background-color:#963232;color:#fff;}
+body details summary var.pass,body details summary var.on{background-color:green;color:#fff;}
diff --git a/docs/styles/presto.less b/docs/styles/presto.less
new file mode 100644
index 0000000..dcf9f52
--- /dev/null
+++ b/docs/styles/presto.less
@@ -0,0 +1,148 @@
+@import 'elements.less';
+
+@hl: rgba(150, 50, 50, 100);
+@bad: rgba(150, 50, 50, 100);
+@good: rgba(50, 150, 50, 100);
+@light: rgba(0,0,0,.25);
+@med: rgba(0,0,0,.5);
+@shadow: rgba(0,0,0,.75);
+
+body {
+ font: 115%/150% "freight-sans-pro", "Helvetica Neau", Helvetica;
+
+ /* Page layout, grid, and colours */
+ header, section, footer {
+ width: 100%;
+ position: relative;
+ }
+ header>div, section>div, footer>div {
+ position: relative;
+ width: 40em;
+ margin: 0 auto;
+ }
+
+ /* Overrides */
+
+ > a {
+ position: fixed; right: 0; top: 0; z-index: 999;
+ display: block;
+ width: 125px; height: 125px;
+ }
+ a, a:visited, a:hover {
+ color: @hl;
+ text-decoration: none;
+ }
+ h1, h2, h4, h5 {
+ margin: 2em 0 0;
+ }
+ h3 {
+ margin: 1.75em 0 0;
+ font-size: smaller;
+ font-weight: 400;
+ text-transform: uppercase;
+ }
+
+ code, pre {
+ font: 10pt/100% Menlo, Monaco, "Andale Mono", monospace;
+ }
+ pre {
+ background-color: rgba(0,0,0,.1);
+ padding: .75em 1em;
+ .rounded(.5em);
+ overflow: hidden;
+ }
+ pre.highlight {
+ border: 0 !important;
+ padding: .75em !important;
+ width: 97% !important;
+ font: 10pt/100% Menlo, Monaco, "Andale Mono", monospace !important;
+ }
+
+ p {
+ margin: .25em 0 1em;
+ }
+ p>code {
+ background-color: rgba(0,0,0,.1);
+ padding: .35em .25em; margin: 0 .25em 0 0;
+ .rounded(.25em);
+ }
+
+
+ /* Sections */
+
+ header {
+ h1 {
+ margin: 0 0 .5em;
+
+ em {
+ display: block;
+ font-size: 10pt;
+ font-style: normal;
+ font-weight: lighter;
+ color: #333;
+ }
+ }
+
+ nav {
+ position: absolute; right: 0; top: .5em;
+ width: auto;
+
+ a {
+ display: block; float: left;
+ width: 5em; line-height: 95%;
+ margin-right: 1em; padding-bottom: .25em;
+ font-size: 11pt;
+ font-weight: lighter;
+ text-align: center;
+ text-transform: uppercase;
+ border-bottom: 4px solid transparent;
+ }
+ a:hover {
+ border-color: @hl;
+ }
+ }
+ }
+
+ footer {
+ div {
+ border-top: 1px solid @light;
+ margin-top: 3em;
+
+ h5 {
+ font-weight: 200;
+ }
+ }
+ }
+
+ /* Test blocks */
+
+ details:nth-child(1) {
+ margin-top: 2em;
+ }
+
+ details {
+ border: 1px solid rgba(0,0,0,.25);
+ padding: .25em .5em; margin: .25em .5em;
+
+ summary::-webkit-details-marker { content: '...'; }
+
+ summary {
+ position: relative;
+ font-weight: bold;
+ outline: none;
+ cursor: pointer;
+
+ var {
+ position: absolute; right: 0; top: 0;
+ width: 3em;
+ text-align: center;
+ font-style: normal;
+ font-weight: bold;
+ background-color: @light; color: @med;
+ }
+ var.fail, var.off { background-color: @bad; color: #fff; }
+ var.pass, var.on { background-color: green; color: #fff; }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/docs/tutorial.php b/docs/tutorial.php
new file mode 100644
index 0000000..61520e3
--- /dev/null
+++ b/docs/tutorial.php
@@ -0,0 +1,16 @@
+
+http://presto.napkinware.com/docs/example.json
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/.htaccess b/examples/.htaccess
deleted file mode 120000
index 0ce9f30..0000000
--- a/examples/.htaccess
+++ /dev/null
@@ -1 +0,0 @@
-../lib/htaccess-example
\ No newline at end of file
diff --git a/examples/index.php b/examples/index.php
deleted file mode 120000
index 8bae94b..0000000
--- a/examples/index.php
+++ /dev/null
@@ -1 +0,0 @@
-../lib/install-checklist.php
\ No newline at end of file
diff --git a/lib/_config.php b/lib/_config.php
index 9fb5f0d..7593497 100644
--- a/lib/_config.php
+++ b/lib/_config.php
@@ -1,10 +1,12 @@
-line = $line;
$e->file = $file;
throw $e;
}
-}
+}
// Set up PHP error handling (note these settings are overriden by explicit PHP ini settigns, we should address this)
assert_options(ASSERT_WARNING, 0);
+assert_options(ASSERT_QUIET_EVAL, 1);
+if (PRESTO_DEBUG) {
+ assert_options(ASSERT_ACTIVE, 1);
+ assert_options(ASSERT_WARNING, 1);
+ assert_options(ASSERT_BAIL, 0);
+}
ini_set('html_errors', false);
error_reporting(E_ALL);
set_error_handler(array("PrestoException", "errorHandlerCallback"), E_ALL);
+// Create a handler function
+function presto_assert_handler($file, $line, $code, $description = 'no description available') {
+ if (empty($code)) $code = 0;
+ $message = "Assert failed in $file:$line with #$code - $description.";
+ error_log($message);
+ PrestoException::errorHandlerCallback(500, $message, $file, $line, NULL);
+}
+
+// Register Presto assert handling
+assert_options(ASSERT_CALLBACK, 'presto_assert_handler');
+
diff --git a/lib/_helpers.php b/lib/_helpers.php
index 7f68f50..0756ad3 100644
--- a/lib/_helpers.php
+++ b/lib/_helpers.php
@@ -1,9 +1,24 @@
-concepts[] = $concept;
- }
+ if (func_num_args() == 2) throw new Exception('Code upgrade required (Presto base classes have changed).', 500);
}
+
/* Attach to Presto framework */
- public function attach($ctx, $resp, $req) {
+ public static function attach($ctx, $resp, $req) {
self::$ctx = $ctx;
self::$resp = $resp;
self::$req = $req;
@@ -49,53 +38,32 @@ public function add_header($key, $value) {
$this->headers[$key] = $value;
}
public function headers() { return $this->headers; }
-
- /* Test if a route refers to a valid concept (member) */
- public function is_valid_concept($c) { return !empty($this->concepts)
- && in_array($c, $this->concepts); }
-
- /** Advanced route mapping
-
- Adds a mapping between a URI pattern and a callback to delegate to. Used to route complex
- sub delegates, for things like hierarchical resources.
- */
- public function add_delegate($regex, $delegateFn) {
-
- // check for conflicts in previously added delegates.
- if (array_key_exists($regex, $this->delegates))
- throw new Exception("URI delegate already exists: pattern collision for '$regex'", 500);
-
- // preflight (and compile + cache) the regex ... errors handled by Presto.
- if (!preg_match($regex, '')) $this->delegates[$regex] = $delegateFn;
+
+ /* Restrict the valid contentTypes for this API or API route */
+ public function restrictTo($types) {
+ return $this->supports_contentType($types);
}
+
+ /* Set necessary CORS headers for this API route.
- /* Do delegation for hierarchical sub-routes
-
- Provides internal delegation to registered callbacks.
-
- Throws if delegation fails.
+ * Sets `Access-Control-Allow-Origin` appropriately with respect to referer (necessary for `Access-Control-Allow-Credentials`)
+ * Sets `Access-Control-Allow-Credentials: true`: allow `rx-auth` cookie to be included in request (authentication)
+ * Sets `Access-Control-Allow-Methods: GET`: only GETs supported for now
+ * sets some other useful default headers
+
*/
- public function delegate($ctx, $data = null) {
- if (empty($this->delegates) || empty($ctx) || empty($ctx->params))
- throw new Exception('Unserviceable internal delegation attempt.', 501);
+ public function allowCrossOrigin() {
+
+ if (empty($_SERVER['HTTP_ORIGIN']))
+ return; // This is not a CORS request
- $path = implode('/', $ctx->params);
- foreach ($this->delegates as $p => $d) {
- if (preg_match($p, $path)) {
- if (empty($data)) return $this->$d($ctx);
- else return $this->$d($ctx, $data);
- }
- }
- throw new Exception("Bad request. No sub method exists for resource $path", 404);
+ $this->add_header('Access-Control-Allow-Origin', $_SERVER['HTTP_ORIGIN']);
+ $this->add_header('Access-Control-Allow-Credentials', 'true');
+ $this->add_header('Access-Control-Allow-Methods', 'GET');
+ $this->add_header('Access-Control-Allow-Headers', 'Content-Type,Accept');
+ $this->add_header('Access-Control-Max-Age', '10');
}
-
- /* Restrict the valid contentTypes for this API or API route
-
- */
- public function restrictTo($types) {
- return $this->validate_contentType($types);
- }
-
+
/* Get a filtered variable (get filter_var_array + exceptions) */
static function filtered($thing, $rules, $defaults = null) {
if ($defaults) $thing = array_merge($defaults, (array)$thing);
@@ -106,18 +74,18 @@ static function filtered($thing, $rules, $defaults = null) {
if ( $missing = array_filter( $filtered, function($v) { return $v === null; } ) )
throw new Exception("Missing parameter(s): " . implode(array_keys($missing), ', '), 406);
-
+
if ( $invalid = array_filter( $filtered, function($v) { return is_bool($v) && $v === FALSE; } ) )
throw new Exception("Invalid parameter(s): " . implode(array_keys($invalid), ', '), 406);
return (object) $filtered;
}
-
- private function validate_contentType($t) {
+
+ private function supports_contentType($t) {
$in = self::$ctx->class . '::' . self::$ctx->method . '()';
- $res = self::$ctx->res;
-
- if (!is_array($t)) $t = array($t);
- if (!in_array($res, $t)) throw new Exception("Unsupported media type $res for $in.", 415);
+ $type = self::$ctx->type;
+
+ if (!is_array($t)) $t = array($t);
+ if (!in_array($type, $t)) throw new Exception("Unsupported media type '$type' for '$in'.", 415);
}
}
diff --git a/lib/auth_token.php b/lib/auth_token.php
index 74957c2..3b0c583 100644
--- a/lib/auth_token.php
+++ b/lib/auth_token.php
@@ -1,5 +1,5 @@
-build($v, false /* force creation of check + timestamp */ );
- $this->encrypt();
+ $this->encrypt();
} elseif (is_string($v)) {
// build from encrypted string
$this->t = $v;
- $this->decrypt();
+ $this->decrypt();
} else
throw new Exception('Missing auth_token or token parts.', 500);
}
-
+
/* Get the encoded token */
public function encoded() { $this->encrypt(); return $this->t; }
-
+
/* Get the token parts */
public function parts() {return $this->p;}
-
+
/* Is this token valid? */
public function ok() {
- return count($this->p) && !empty($this->p->h)
+ return count($this->p) && !empty($this->p->h)
&& $this->p->h == $this->hash;
}
-
- /* ---------------------------- internals ---------------------------- */
-
- /* Get the checked parts as a string (for calculating checksums) */
+
+ /* Get the checked parts as a string (for calculating checksums) */
private function checked_parts() {
if (empty($this->p)) throw new Exception('Token is not initialized.', 500);
- $elements = array($this->p->name, $this->p->email, $this->p->id, $this->p->acct,
- TOKEN_HASH_SECRET, $this->p->t);
-
+ $elements = array($this->p->name, $this->p->email, $this->p->id, $this->p->acct,
+ TOKEN_HASH_SECRET, $this->p->t);
+
foreach ($elements as &$e) $e = urlencode($e);
return implode('', $elements);
}
- /* Get a token as an encoded URI string */
+ /* Get a token as an encoded URI string */
private function uri() {
if (empty($this->p)) throw new Exception('Token is not initialized.', 500);
$uri = '';
foreach ($this->p as $k => $v) $uri .= $k.'='.urlencode($v).'&';
- return $uri;
+ return $uri;
}
-
- /* Build the token object from parts
-
+
+ /* Build the token object from parts
+
A strict build checks that all parts are provided
-
+
Returns false if the token build has failed.
*/
private function build($p, $strict = true) {
// apply defaults
- $this->set($p['t'], time(), $strict);
- $this->set($p['w'], 60, $strict);
+ if (!array_key_exists('t', $p)) $this->set($p['t'], time(), $strict);
+ if (!array_key_exists('w', $p)) $this->set($p['w'], 60, $strict);
// check for required elements
- foreach (array('name', 'id', 'acct', 'a', 'c', 's') as $k)
+ foreach (array('name', 'id', 'acct', 'a', 's') as $k)
if (empty($p[$k])) // missing a required token element
throw new Exception('Invalid credentials, missing: '.$k, 401);
-
+
if (empty($p['email']) && empty($p['key']))
throw new Exception('Invalid credentials, missing email and key, at least one is required', 401);
foreach ($p as $k => &$v) $v = urldecode($v); // remove URI encoding
-
+
$this->p = (object) $p; // objectize for convinience
// extract capabilities+roles
- if (strlen($this->p->c)) {
+ if (!empty($this->p->c) && strlen($this->p->c)) {
$roles = explode(',', $this->p->c);
foreach ($roles as $tuple) {
$cap = explode('/', $tuple);
@@ -110,37 +108,37 @@ private function build($p, $strict = true) {
$this->roles[$role] = array_keys($list);
}
}
-
+
// lastly, apply a default value to the CRC (if needed/possible)
- $this->set($this->p->h, sha1($this->checked_parts()), $strict);
+ $this->set($this->p->h, sha1($this->checked_parts()), $strict);
$this->hash = sha1($this->checked_parts());
-
+
return $this->hash == $this->p->h;
}
-
- /* Assign a token value a default (if possible) */
+
+ /* Assign a token value a default (if possible) */
private function set(&$v, $d, $strict = true) {
if (!empty($v))
return;
-
- if (!$strict) $v = $d;
+
+ if (!$strict) $v = $d;
else throw new Exception('Missing required check field.', 401);
}
-
+
/* Encrypt a token from parts */
- private function encrypt() {
- $this->t = openssl_encrypt($this->uri(),
+ private function encrypt() {
+ $this->t = openssl_encrypt($this->uri(),
TOKEN_ENCRYPTION, TOKEN_SIGNING_KEY, false, SIGNING_INIT);
-
+
return $this->t;
}
-
+
/* Decrypt a token into parts (thows on errors) */
- private function decrypt() {
+ private function decrypt() {
if (empty($this->t))
throw new Exception('Token is empty.', 401);
- $t = openssl_decrypt($this->t,
+ $t = openssl_decrypt($this->t,
TOKEN_ENCRYPTION, TOKEN_SIGNING_KEY, false, SIGNING_INIT);
if (empty($t))
@@ -150,10 +148,10 @@ private function decrypt() {
if (empty($p))
throw new Exception('Token format invalid.', 401);
-
+
if (!$this->build( $p ))
- throw new Exception('Token integrity check failed.', 401);
-
+ throw new Exception('Token integrity check failed.', 401);
+
return $this->p;
}
diff --git a/lib/autoloader.php b/lib/autoloader.php
new file mode 100644
index 0000000..fef7192
--- /dev/null
+++ b/lib/autoloader.php
@@ -0,0 +1,50 @@
+container;
+ $error = "API `$call->class` not found";
+
+ if ( !stream_resolve_include_path($call->file) ) {
+ $extra = " ({$call->file} not found).";
+
+ if ( !empty($in) && !is_dir($in) )
+ $extra = " ({$call->file} not found, $in missing)."; // aid debugging of routes-in-folders
+
+ throw new Exception($error . $extra, 404);
+ }
+
+ if ( !(require_once $call->file) )
+ throw new Exception($error . " ({$call->file} not loadable).", 500);
+
+
+}
diff --git a/lib/db.php b/lib/db.php
index c7af506..946bfd0 100644
--- a/lib/db.php
+++ b/lib/db.php
@@ -5,95 +5,334 @@
See PDO docs for details: http://www.php.net/manual/en/class.pdo.php
Adds a few extensions for common batched operations.
-
+
See related `extra/tagged-sql.php` for extra post-processing magic.
*/
class db extends PDO {
- private $statement;
- const USR_DEF_DB_ERR = '45000';
+ private $statement;
+ private static $valid_pdo_types = array(PDO::PARAM_INT, PDO::PARAM_NULL,
+ PDO::PARAM_BOOL, PDO::PARAM_STR);
+ const USR_DEF_DB_ERR = '45000';
const DB_NO_ERR = '00000';
+
+
/* Create (or reuse) an instance of a PDO database */
- static function _instance($dsn, $user, $password, $config = null) {
- global $_db;
- if ($_db !== null) return $_db; // return cached
-
- if ($config === null)
- $config = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' );
-
- try {
- $_db = new db($dsn, $user, $password, $config);
- } catch (Exception $e) {
+ static function _instance($dsn = null, $user = null, $password = null, $config = null) {
+ global $_db;
+ if ($_db !== null) return $_db; // return cached
+
+ if ($config === null)
+ $config = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' );
+
+ try {
+ $_db = new db($dsn, $user, $password, $config);
+ $_db->setAttribute(constant("PDO::ATTR_ERRMODE"), constant("PDO::ERRMODE_EXCEPTION"));
+ } catch (Exception $e) {
throw new Exception("Failed to connect to database.", 500, $e);
- }
-
- return $_db;
+ }
+
+ return $_db;
}
-
+
/* Returns an array of classes based on the given SQL and bound parameters (see PDO docs for details) */
- function select($sql, $bound_parameters = array()) {
+ function select($sql, $bound_parameters = array()) {
+
+ // Expand any array parameters
+ $this->expand_select_params($sql, $bound_parameters);
+
$this->statement = $this->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$this->statement->execute($bound_parameters);
$resultset = $this->statement->fetchAll(PDO::FETCH_CLASS);
$this->errors();
return $resultset;
}
+ /* Return a single row (throws on > 1 row) */
function select_row($sql, $bound_parameters = array()) {
- $r = $this->select($sql, $bound_parameters);
- $c = count($r);
- if ($c === 0) throw new Exception("Found 0 rows", 500);
- elseif ($c !== 1) throw new Exception("Too many rows (".(count($r)).") returned from '$sql'", 500);
- return $r[0];
+ $r = $this->select($sql, $bound_parameters);
+ $c = count($r);
+ if ($c === 0) throw new Exception("Found 0 rows", 500);
+ elseif ($c !== 1) throw new Exception("Too many rows (".(count($r)).") returned from '$sql'", 500);
+ return $r[0];
}
-
+
+ /* Return a simple type and object mapped set of records
+
+ Uses column aliases and type designation to generate object hierarchy.
+
+ Features:
+
+ * simple key format `any.number.of.subkeys:optional_type`
+ * order of columns is not important
+ * allows values to be type cast
+
+ Not supported:
+
+ * combining rows into sub-objects
+
+ Example:
+
+ SELECT
+ SomeID AS `id:int`,
+ FirstName AS `name.first`,
+ LastName AS `name.last`,
+ AnotherColumn AS `other`
+ FROM SomeTable
+
+
+ [{
+ id: 1234,
+ name: {
+ 'first': "Sideshow",
+ 'last': "Bob"
+ },
+ other: "some value"
+ }, ...]
+ */
+ function select_objects($sql, $bound_parameters = array()) {
+
+ $rows = $this->select($sql, $bound_parameters);
+
+ $d = '.'; $t = ':'; // delimiters for depth and type
+ $o = array(); // output
+
+ foreach ($rows as $r) { // each row in result set
+
+ $row = array();
+ $type = '';
+
+ foreach ($r as $key => $value) { // each column
+
+ if (strpos($key, $t)) {
+ // extract type
+ $p = explode($t, $key);
+ $type = $p[1];
+ $key = $p[0];
+ } else {
+ $type = null; // no type provided
+ }
+
+ // extract keys
+ $keys = strpos($key, $d) ? explode($d, $key) : array($key);
+
+ $ptr = &$row; // working pointer
+
+ // create objects as needed
+ foreach ($keys as $k) {
+ if (!isset($ptr[$k])) $ptr[$k] = array();
+ $ptr = &$ptr[$k];
+ }
+
+ // adjust type
+ if (!empty($type)) settype($value, $type);
+
+ // add column
+ if (empty($ptr)) $ptr = $value;
+ else $ptr[] = $value;
+ }
+
+ $o[] = db::array_to_object($row); // add row to output as objects
+ }
+
+ return $o;
+ }
+
/* Provide a wrapper for updates which is really just an alias for the query function. */
- function update($sql, &$bound_parameters = array()) {
+ function update($sql, $bound_parameters = array()) {
$this->query($sql, $bound_parameters);
- if ($this->statement->rowCount() === 0)
- throw new Exception('Update failed: resource was not found.', 404);
+ return $this->statement->rowCount() > 0; // optional success condition, leave to caller to decide
}
- /*
+ /*
General query. Use this when no specific results are expected. Example: attempting to delete a resource
that may or may exist.
-
- $bound_parameters is an array of arrays with the 'value' member as the value to bind and the 'pdoType' as
+
+ $bound_parameters is an array of arrays with the 'value' member as the value to bind and the 'pdoType' as
that parameter's PDO datatype.
*/
- function query($sql, &$bound_parameters = array()) {
+ function query($sql, $bound_parameters = array()) {
+ // Check if the params have been bound or not already
+ if (!$this->is_bound($bound_parameters))
+ $bound_parameters = $this->bind_parameters($bound_parameters);
+
+ // Expand any array parameters
+ $this->expand_query_params($sql, $bound_parameters);
+
$this->statement = $this->prepare($sql);
- if (!empty($bound_parameters)) {
+ if (!empty($bound_parameters)) {
foreach ($bound_parameters as $key => &$param) {
$v = $param['value']; $t = $param['pdoType'];
if (!$this->statement->bindValue($key, $v, $t))
throw new Exception("Unable to bind '$v' to named parameter ':$key'.", 500);
}
}
-
- $this->statement->execute();
- $this->errors();
+
+ try {
+ $this->statement->execute();
+ $this->errors();
+ } catch (Exception $e) {
+ $this->errors(); // force errors() to run even after exceptions (for PDO::ERRMODE_EXCEPTION)
+ }
+
}
-
+
/* Provide a wrapper for inserts which is really just an alias for the query function. */
- function insert($sql, &$bound_parameters = array()) {
+ function insert($sql, $bound_parameters = array()) {
$this->query($sql, $bound_parameters);
if ($this->statement->rowCount() === 0)
- throw new Exception('Insert failed: no rows were inserted.', 500);
+ throw new Exception('Insert failed: no rows were inserted.', 409);
}
-
+
+ /*
+ Manage a multiple insertion
+
+ * `$sql`: an `INSERT` statement of the form `INSERT INTO Foo (C1, ..., Cn) VALUES (:key1, ..., :keyn)`
+ * `$dataTypes`: an array containing the PDO data types of the data values of the form
+ `array(0 => PDO::dataType, ..., n => PDO::dataType)`
+ * `$data`: a 2D array of the form `array(0 => array(key1 => v01, ..., keyn => v0n), ..., m => array(key1 => vm1, ..., keyn => vmn))`
+
+ 1. Handled with `m` `INSERT` statements wrapped in a transaction.
+ 2. The `INSERT` is prepared first (via PDO).
+ 3. `m` `INSERT` statements are then executed, binding parameters at execution time.
+ */
+ public function multi_insert($sql, $dataTypes, $data) {
+
+ if (empty($data))
+ throw new Exception('Aborting: no data to insert.', 500);
+
+ if (empty($dataTypes))
+ throw new Exception('Aborting: no data-types provided for data values (necessary for binding).', 500);
+
+ try {
+ $this->beginTransaction();
+
+ $this->statement = $this->prepare($sql);
+
+ foreach ($data as $i => $row) {
+ if (count($row) !== count($dataTypes))
+ throw new Exception("Transaction rolled back as parameter count does not match data value count for data row $i", 500);
+
+ $index = 0;
+ foreach ($row as $key => $v) {
+ $t = $dataTypes[$index];
+ if (!$this->statement->bindValue($key, $v, $t))
+ throw new Exception("Unable to bind '$v' to named parameter ':$key'.", 500);
+ $index++;
+ }
+ $this->statement->execute();
+ $this->errors();
+ }
+
+ $this->commit();
+ }
+ catch (Exception $e) {
+ $this->rollBack();
+ $m = $e->getMessage();
+ throw new Exception("Transaction failed and rolled back: $m", 500);
+ }
+ }
+
/* Provide a wrapper for deletes which is really just an alias for the query function. */
- function delete($sql, &$bound_parameters = array()) {
+ function delete($sql, $bound_parameters = array()) {
$this->query($sql, $bound_parameters);
if ($this->statement->rowCount() === 0)
throw new Exception('Delete failed: resource does not exist.', 404);
}
-
+
/* Return the number of rows affected by the last INSERT, DELETE, or UPDATE. */
function affected_rows() {
return $this->statement->rowCount();
}
-
+
+ /*
+ Generates a PDO bound parameterized array.
+
+ Pass this an array of keys and values that you want to use in your DB query.
+ generate_params() will return you a valid PDO array to use with
+ your INSERT and UPDATE statements.
+
+ View the test harness for this here: https://gist.github.com/ngallagher87/6717925
+
+ Supported types:
+ =================
+ PARAM_BOOL
+ PARAM_NULL
+ PARAM_INT
+ PARAM_STR
+
+ Unsupported types:
+ =================
+ PARAM_LOB
+ PARAM_INPUT_OUTPUT
+ PARAM_STMT (No drivers support this anyways)
+
+ Note:
+
+ If you need to use one of these unsupported types, you'll have to
+ generate the params by hand.
+
+ Example:
+ ========
+
+ $sql = << 'tuesday',
+ 'dayNumber' => 2,
+ 'isHoliday' => true
+ );
+ $this->db->insert($sql, $params);
+ */
+ public function bind_parameters($array) {
+ $find_type = function($val) {
+ $pdoType = PDO::PARAM_NULL;
+ if (is_numeric($val)) $pdoType = PDO::PARAM_INT;
+ else if (is_bool($val)) $pdoType = PDO::PARAM_BOOL;
+ else if (is_string($val)) $pdoType = PDO::PARAM_STR;
+ return $pdoType;
+ };
+
+ $params = array();
+ foreach ($array as $key => $val) {
+ $pdoType = PDO::PARAM_NULL;
+ if (is_array($val)) {
+ $type = $find_type(current($val));
+ foreach ($val as $k => $v) {
+ if ($find_type($v) !== $type)
+ throw new Exception('Array contents must have the same type. '.
+ 'Cannot bind parameters', 400);
+ }
+ $pdoType = $type;
+ } else {
+ $pdoType = $find_type($val);
+ }
+ $params[$key] = array('value' => $val, 'pdoType' => $pdoType);
+ }
+ return $params;
+ }
+
+ /*
+ Tests to see if parameters have been bound or not.
+ Returns true if they are bound, and false if they are not.
+
+ An array is either bound or not - there are no partial cases.
+ */
+ private function is_bound($array) {
+ foreach ($array as $key => $val) {
+ if (is_array($val)) {
+ if (!isset($val['pdoType']) ||
+ !in_array($val['pdoType'], self::$valid_pdo_types))
+ return false;
+ }
+ else return false;
+ }
+ return true;
+ }
+
/* Throw error info pertaining to the last operation. */
private function errors() {
$e = $this->statement->errorInfo();
@@ -105,6 +344,125 @@ private function errors() {
else if (!empty($e[0]) && !empty($e[2])) {
throw new Exception($e[2], 500);
}
+ else if (!empty($e[0]) && strcmp($e[0], 'HY093') === 0) {
+ throw new Exception('Error HY093: Check your PDO field bindings', 500);
+ }
else throw new Exception('Update failed for unknown reason.', 500);
}
-}
\ No newline at end of file
+
+ /*
+ Utility: expand any parameters passed in as an array (as PDO does not yet support this).
+
+ For any parameter `p => array( k1 => v1, k2 => v2, ..., kn => vn)` in `$params`,
+ `p` is expanded to `p_k1 => v1, p_k2 => v2, ..., p_kn => vn` all members of `$params`.
+
+ For any label `:p` in `$sql`, `p` is replaced with `:p_k1, :p_k2, ..., :p_kn` in `$sql`.
+ */
+ private function expand_select_params(&$sql, &$params) {
+
+ $expanded = array(); // store expanded parameters until we're done
+ $expandedKeys = array(); // store the keys of expanded arrays so we can unset them when we're done
+
+ foreach ($params as $p => $arrayParam) {
+
+ // only worry about arrays
+ if (!is_array($arrayParam)) continue;
+
+ // Empty arrays need to be handled explicitly so they don't cause array to string conversion exceptions at bind time.
+ if (empty($arrayParam)) {
+ $params[$p] = '';
+ continue;
+ }
+
+ $expandedKeys[] = $p;
+ $names = ''; // list of labels `:p_k1, :p_k2, ..., :p_kn` with which to replace `:p` in $sql
+ foreach ($arrayParam as $k => $v) {
+
+ // Ensure validity of members
+ if ((!empty($v) && !is_scalar($v))) {
+ $t = gettype($v);
+ throw new Exception("Array parameter expansion error: members of array '$p' must be scalars, but '$k' is a '$t'.", 500);
+ }
+
+ $expanded["{$p}_{$k}"] = $v;
+ $names .= ":{$p}_{$k}, ";
+ }
+
+ // replace :p in $sql with $names
+ $names = rtrim($names, " ,");
+ $sql = str_replace(":$p", $names, $sql);
+ }
+
+ // Nothing expanded
+ if (empty($expandedKeys)) return;
+
+ // Get rid of the now expanded array params
+ foreach ($expandedKeys as $v)
+ unset($params[$v]);
+
+ // Merge in the expanded values
+ $params = array_merge($params, $expanded);
+ }
+
+ /*
+ Utility: expand any parameters passed in as an array (as PDO does not yet support this).
+
+ For any parameter `p => array('value' => array( k1 => v1, k2 => v2, ..., kn => vn), 'pdoType' => PDO::SOME_PDO_TYPE)` in `$params`,
+ `p` is expanded to
+ `p_k1 => array('value' => v1, 'pdoType' => PDO::SOME_PDO_TYPE),
+ p_k2 => array('value' => v2, 'pdoType' => PDO::SOME_PDO_TYPE),
+ ...,
+ p_kn => array('value' => vn, 'pdoType' => PDO::SOME_PDO_TYPE)`
+ all members of `$params`.
+
+ For any label `:p` in `$sql`, `p` is replaced with `:p_k1, :p_k2, ..., :p_kn` in `$sql`.
+ */
+ private function expand_query_params(&$sql, &$params) {
+
+ $expanded = array(); // store expanded parameters until we're done
+ $expandedKeys = array(); // store the keys of expanded arrays so we can unset them when we're done
+
+ foreach ($params as $p => $arrayParam) {
+
+ // only worry about arrays
+ if (!is_array($arrayParam['value'])) continue;
+
+ // Empty arrays need to be handled explicitly so they don't cause array to string conversion exceptions at bind time.
+ if (empty($arrayParam['value'])) {
+ $params[$p] = array('value' => '', 'pdoType' => PDO::PARAM_STR);
+ continue;
+ }
+
+ $expandedKeys[] = $p;
+ $names = ''; // list of labels `:p_k1, :p_k2, ..., :p_kn` with which to replace `:p` in $sql
+ foreach ($arrayParam['value'] as $k => $v) {
+
+ // Ensure validity of members
+ if ((!empty($v) && !is_scalar($v))) {
+ $t = gettype($v);
+ throw new Exception("Array parameter expansion error: members of array '$p' must be scalars, but '$k' is a '$t'.", 500);
+ }
+
+ $expanded["{$p}_{$k}"] = array('value' => $v, 'pdoType' => $arrayParam['pdoType']);
+ $names .= ":{$p}_{$k}, ";
+ }
+
+ // replace :p in $sql with $names
+ $names = rtrim($names, " ,");
+ $sql = str_replace(":$p", $names, $sql);
+ }
+
+ // Nothing expanded
+ if (empty($expandedKeys)) return;
+
+ // Get rid of the now expanded array params
+ foreach ($expandedKeys as $v)
+ unset($params[$v]);
+
+ // Merge in the expanded values
+ $params = array_merge($params, $expanded);
+ }
+
+ // convert an array to an object (recursively)
+ public static function array_to_object($o) { return is_array($o) ? (object) array_map(__METHOD__, $o) : $o; }
+}
diff --git a/lib/delegator-index.php b/lib/delegator-index.php
index e1eaacc..815b44d 100644
--- a/lib/delegator-index.php
+++ b/lib/delegator-index.php
@@ -1,16 +1,24 @@
getCode();
$message = $e->getMessage();
$via = $e->getPrevious();
-
+
if (PRESTO_DEBUG) {
$detail = (is_object($p)) ? $p::call : $e->getTrace();
$payload = array('message' => $message , 'code' => $n, 'detail' => $detail);
@@ -20,7 +28,9 @@
if ($via) $payload['error'] = array('message' => $via->getMessage(), 'code' => $via->getCode());
- error_log(json_encode($payload)); // also send to syslop
+ if (PRESTO_TRACE) $payload[PRESTO_TRACE_KEY] = Presto::trace_info();
+
+ error_log(json_encode($payload)); // also send to syslog
header("HTTP/1.0 $n API error");
header("Content-Type: application/json");
diff --git a/lib/encoders/html.php b/lib/encoders/html.php
new file mode 100644
index 0000000..c7622f3
--- /dev/null
+++ b/lib/encoders/html.php
@@ -0,0 +1,41 @@
+ array('body' => $node)));
+ }
+
+ $indent = str_repeat("\t", $d); // indent for pretty printing
+
+ if (is_string($node)) return print "\n$indent$node";
+ elseif (is_array($node)) {
+
+ // descend into child nodes
+
+ $d++;
+ foreach ($node as $k => &$v) {
+ $a = '';
+
+ if (empty($k) || is_numeric($k))
+ $k = 'li'; // assume lists are LIs
+
+ if (is_callable($mapper))
+ $k = $mapper($k, $v, $a, $d);
+
+ // print node
+
+ print "\n$indent<$k$a>";
+ _encode_html($v); // recurse
+ print "\n$indent$k>";
+ }
+ $d--;
+ }
+}
+
diff --git a/lib/extras/tagged-sql.php b/lib/extras/tagged-sql.php
deleted file mode 100644
index 4929eb2..0000000
--- a/lib/extras/tagged-sql.php
+++ /dev/null
@@ -1,219 +0,0 @@
- Array
- (
- [@attributeName] => attributeValue
- )
- ~~~
-
- Simple node tags add a simple node to the current (complex) node of the
- DOM. Simple nodes will have a value and may have a label or classes, but that's it,
- they do not contain complex nodes or other attributes. They are leaves in the DOM:
- ~~~
- [simpleNode] => Array
- (
- [@label] => Human readable label,
- [@class] => class1 class2 class3 ... classn
- [value] => The value pulled from the DB
- )
- ~~~
-
- ## Result set row ordering
-
- The transform depends on the row ordering of the result set. The purpose of this
- transform is to take flat result sets and transform them into the tree they represent.
-
- For example the result set:
- ~~~
- | RubricID | DomainID | ElementID |
- | -------- | -------- | --------- |
- | 1 | 1 | 1 |
- | 1 | 1 | 2 |
- | 1 | 2 | 3 |
- | 1 | 2 | 4 |
- ~~~
-
- Is transfomed into the DOM:
- ~~~
-
- |¯¯¯¯¯¯¯¯|
- | Rubric |
- | @id:1 |
- |________|
- |
- | |¯¯¯¯¯¯¯¯|
- |------->| Domain |------------->|¯¯¯¯¯¯¯¯¯|
- | | @id:1 | | | Element |
- | |________| | | @id:1 |
- | | |_________|
- | |
- | |----->|¯¯¯¯¯¯¯¯¯|
- | | Element |
- | | @id:2 |
- | |_________|
- |
- | |¯¯¯¯¯¯¯¯|
- |------->| Domain |------------->|¯¯¯¯¯¯¯¯¯|
- | @id:2 | | | Element |
- |________| | | @id:3 |
- | |_________|
- |
- |----->|¯¯¯¯¯¯¯¯¯|
- | Element |
- | @id:4 |
- |_________|
-
-
-
- ~~~
-
- Note that if you do not order the rows in your result set in this way then the
- there is no guarantee about the shape of the resulting DOM, except that it will
- be wrong.
-
- The $stripMeta flag determines whether attributes will begin with the '@' symbol.
- You may want this symbol stripped off if, for instance, you will be transmitting
- the result of this call as JSON data to a client. This flag has no effect on the
- special '@class' and '@label' reserved keys.
-
- */
- public function &dominate(&$rs, $asObj = true, $stripMeta = false) {
- $dom = array();
- if (empty($rs)) return $dom;
-
- $r_constructorNode = '/^#(\w+)(?:\(.*\))?(?:\[(.+)\])?$/';
- $r_simpleNode = '/^(@?\w+)(?:\(.*\))?(?:\[(.+)\])?$/';
- $r_classes = '/^.*\((.*)\).*$/';
-
- foreach ($rs as $row) {
- $currentNode = &$dom;
- foreach ($row as $key => $value) {
-
- if (!isset($value)) $value = ''; // clamp null values to '';
-
- // get the node level classes (if any)
- $classes = null;
- if (preg_match($r_classes, $key, $matches)) {
- $classes = $matches[1];
- }
-
- if (preg_match($r_constructorNode, $key, $matches)) {
- // this tag will construct a node.
- if ($value === '') {
- // no value means no ID ... we can't do anything with this
- break;
- }
-
- $nodeName = $matches[1];
-
- // move into the node
- $currentNode = &$currentNode[$nodeName][$value];
-
- // set the node level class(es)
- if (isset($classes)) {
- $currentNode['@class'] = $classes;
- }
-
- // set the node label if there is one
- if (isset($matches[2]) && trim($matches[2]) != '') {
- $currentNode['@label'] = trim($matches[2]);
- }
- }
- else if (preg_match($r_simpleNode, $key, $matches)) {
- // this tag will construct a simple node (or attribute).
- $nodeName = $matches[1];
-
- // create the node (or attribute)
- if (strpos($nodeName, '@') === 0) {
- // create the attribute
- $nodeName = $stripMeta ? substr($nodeName, 1) : $nodeName;
- $currentNode[$nodeName] = $value;
- }
- else {
- // create the simple node
- $currentNode[$nodeName] = array();
-
- // set the node label if there is one
- if (isset($matches[2]) && trim($matches[2]) != '') {
- $currentNode[$nodeName]['@label'] = trim($matches[2]);
- }
-
- // set the node level class(es)
- if (isset($classes)) {
- $currentNode[$nodeName]['@class'] = $classes;
- }
-
- // set the node value
- $currentNode[$nodeName]['value'] = $value;
- }
- }
- }
- }
- if (empty($dom)) return $dom;
- if ($asObj) return (object) $dom;
- return $dom;
- }
-}
\ No newline at end of file
diff --git a/lib/htaccess-example b/lib/htaccess-example
index 68e6ebd..ce50046 100644
--- a/lib/htaccess-example
+++ b/lib/htaccess-example
@@ -13,7 +13,7 @@ RewriteRule lib/(.*) /404.html [R,L]
# Route API requests
RewriteCond %{REQUEST_FILENAME} !-f
-RewriteRule (.*)\.(.*)$ /delegator.php?r=$1&t=$2& [QSA,L]
+RewriteRule (.*)\.(.*)$ /delegator.php?r=$1&t=$2 [QSA,L]
RewriteRule ^(.*)\.html$ $1.php [NC,L]
# General requests (enable for standard php/index delegate behaviour)
diff --git a/lib/inc.php b/lib/inc.php
index 7470f42..2656392 100644
--- a/lib/inc.php
+++ b/lib/inc.php
@@ -1,3 +1,3 @@
-
-
-
-Presto install checklist
-
-
-
-
-
-
-
Debug mode adds trace and informational logging via syslog and extra detail in exceptions.
+
+
+
+Request details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/setup-tests/info.php b/setup-tests/info.php
new file mode 100644
index 0000000..bd7accb
--- /dev/null
+++ b/setup-tests/info.php
@@ -0,0 +1,71 @@
+_..
+*/
+class info extends API {
+
+ public function __construct() {
+ parent::__construct('presto-example-1');
+ // other startup would go here
+ }
+
+ // info.json (root get request)
+ public function get($p) {
+
+ $this->restrictTo(array('json', 'js'));
+
+ if (count($p) > 1)
+ throw new Exception('Too many parameters', 400); // will result in a proper 400 HTTP status
+
+ return array('example' => 'This is some example information'); // will be returned as json, if json is requested
+ }
+
+ // Test custom header values
+ public function get_header_test($ctx) {
+ $this->restrictTo(array('json', 'js'));
+
+ $this->status(201);
+ $this->add_header('CUSTOM_HEADER', 'TEST');
+ return array('test' => 'ok');
+ }
+
+ // Test binary json values (this should fail)
+ public function get_utf8($ctx) {
+
+ $this->restrictTo(array('json', 'js'));
+ return array('status' => 'fail', 'expected' => 'fail', 'invalidUTF8' => pack("H*" ,'c32e') );
+ }
+
+ // Get the PHP version on this server
+ public function get_php_version() {
+ return array(phpinfo());
+ }
+
+ // Get a test image
+ public function get_image($p, $o, $b, $t) {
+ $this->restrictTo(array('png'));
+ $this->add_header('Content-Type', 'image/png');
+
+ return (string) file_get_contents('test-image.png');
+ }
+
+ // Tests a s2s call
+ public function get_service_test($p, $o, $b, $t) {
+ $s = new service(array(
+ 'service' => 'http://presto.test/setup-tests/'
+ ));
+
+ $d = $s->get_info('header_test.json');
+
+ return array('status' => 'ok', 's2s' => $s);
+ }
+}
\ No newline at end of file
diff --git a/setup-tests/test-image.png b/setup-tests/test-image.png
new file mode 100644
index 0000000..085b5a8
Binary files /dev/null and b/setup-tests/test-image.png differ