--- /dev/null
+# Secret files...
+setup.php
+
+# User data
+db/
+data/
+public/data/
--- /dev/null
+<?php
+
+function cx_require(...$segments) {
+ array_unshift($segments, CX_PATH);
+ require_once(join(DIRECTORY_SEPARATOR, $segments));
+}
+
+define('CX_PATH', __DIR__);
+
+cx_require('lib', 'admin.php');
+cx_require('lib', 'form.php');
+cx_require('lib', 'http.php');
+cx_require('lib', 'images.php');
+cx_require('lib', 'posts.php');
+cx_require('lib', 'sessions.php');
+cx_require('lib', 'setup.php');
+cx_require('lib', 'site.php');
+cx_require('lib', 'system.php');
+cx_require('lib', 'template.php');
+cx_require('lib', 'url.php');
+cx_require('lib', 'user_data.php');
+cx_require('lib', 'users.php');
+
+function cx($db_path, $data_folder_path, $public_data_folder_path) {
+ define('CX_DATABASE_FILE', $db_path);
+ define('CX_USER_DATA_PATH', $data_folder_path);
+ define('CX_PUBLIC_USER_DATA_PATH', $public_data_folder_path);
+
+ if (cx_setup_required()) {
+ cx_setup_run();
+
+ require('../setup.php');
+ $new_author = cx_users_add_user(CX_SETUP_USER, CX_SETUP_PASSWORD);
+ $new_site = cx_sites_add_site(CX_SETUP_URL, CX_SETUP_TITLE, CX_SETUP_BYLINE, CX_SETUP_COPYRIGHT);
+ cx_sites_site_add_user($new_site, $new_author, true);
+
+ exit;
+ }
+
+ $path = '/';
+
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $route_details = parse_url($_SERVER['REQUEST_URI']);
+ if (isset($route_details['path'])) {
+ $path = $route_details['path'];
+ }
+ }
+
+ $script_name = $_SERVER['SCRIPT_NAME'];
+ $script_name_len = strlen($script_name);
+ if (substr_compare($path, $script_name, 0, $script_name_len) == 0) {
+ $path = substr($path, $script_name_len);
+ }
+
+ cx_route($path);
+}
+
+function cx_route($path) {
+ $path_components = explode('/', $path, 10);
+ $path_components = array_filter($path_components);
+ $path_components = array_values($path_components); // re-index
+
+ $template = null;
+ $template_class = 'public';
+ $template_variables = [];
+
+ if (count($path_components) == 0) {
+ $template = 'list';
+ } else if (count($path_components) >= 1 && $path_components[0] == 'feed') {
+ header('Content-type: application/atom+xml;');
+ $template = 'atom';
+ } else if (count($path_components) >= 1 && $path_components[0] == 'cx') {
+ if (count($path_components) >= 2 && $path_components[1] == 'login') {
+ if (cx_admin_logged_in()) {
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ } else {
+ $username = cx_form_input_sanitized('id');
+ $password = cx_form_input_sanitized('password');
+
+ if ($username != null && $password != null && cx_admin_login($username, $password)) {
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ }
+
+ $template_class = 'admin';
+ $template = 'login';
+ }
+ } else {
+ if (cx_admin_logged_in() == false) {
+ cx_http_redirect(cx_url_admin('/login/'));
+ exit(0);
+ } else {
+ if (count($path_components) >= 2 && $path_components[1] == 'logout') {
+ cx_admin_logout();
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ } else if (count($path_components) >= 3 && $path_components[1] == 'posts' && $path_components[2] == 'add') {
+ $template_variables['post_id'] = '0';
+ $template_variables['post_title'] = '';
+ $template_variables['post_slug'] = '';
+ $template_variables['post_date'] = '';
+ $template_variables['post_data'] = '';
+
+ $template_class = 'admin';
+ $template = 'post';
+ } else if (count($path_components) >= 3 && $path_components[1] == 'posts' && $path_components[2] == 'edit') {
+ $post = cx_posts_find_post($_GET['id']);
+
+ $template_variables['post_id'] = $post->id;
+ $template_variables['post_title'] = $post->title;
+ $template_variables['post_slug'] = $post->slug;
+ $template_variables['post_date'] = $post->date;
+ $template_variables['post_data'] = $post->data;
+
+ $template_class = 'admin';
+ $template = 'post';
+ } else if (count($path_components) >= 3 && $path_components[1] == 'posts' && $path_components[2] == 'update') {
+ $title = cx_form_input_sanitized('post_title');
+ $slug = cx_form_input_sanitized('post_slug');
+ if (isset($slug) == false) $slug = null;
+ $date = cx_form_input_sanitized_date_time('post_date');
+ if (isset($date) == false) $date = null;
+ $data = cx_form_input_sanitized('post_data');
+
+ if (isset($_GET['id']) == false or $_GET['id'] == 0) {
+ cx_posts_add_post(1, $title, $slug, $date, $data);
+ } else {
+ $id = $_GET['id'];
+ cx_posts_update_post($id, $title, $slug, $date, $data);
+ }
+
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ } else if (count($path_components) >= 3 && $path_components[1] == 'posts' && $path_components[2] == 'delete') {
+ cx_posts_delete_post($_GET['id']);
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ } else if (count($path_components) >= 3 && $path_components[1] == 'images' && $path_components[2] == 'add') {
+ $template_variables['image_id'] = '0';
+ $template_variables['image_alt_text'] = '';
+
+ $template_class = 'admin';
+ $template = 'image';
+ } else if (count($path_components) >= 3 && $path_components[1] == 'images' && $path_components[2] == 'update') {
+ $alt_text = cx_form_input_sanitized('image_alt_text');
+
+ $filename = $_FILES['image_file']['tmp_name'];
+ $original_filename = $_FILES['image_file']['name'];
+
+ cx_images_add_image(1, $alt_text, $filename, $original_filename);
+
+ cx_http_redirect(cx_url_admin('/'));
+ exit(0);
+ } else {
+ $template_class = 'admin';
+ $template = 'main';
+ }
+ }
+ }
+ } else if (count($path_components) >= 3) { // FIXME sometime, needs more flexibility...
+ $year = $path_components[0];
+ $month = $path_components[1];
+ $slug = $path_components[2];
+
+ $template = 'post';
+ $template_variables['post_id'] = cx_posts_find_post_id($slug);
+ }
+
+ if ($template != null) {
+ $output = cx_template_render($template_class, $template, $template_variables);
+ echo($output);
+ } else {
+ http_response_code(404);
+ exit(0);
+ }
+}
--- /dev/null
+<?php
+
+cx_require('lib', 'sessions.php');
+cx_require('lib', 'users.php');
+
+define('CX_ADMIN_SESSION_LIFETIME', (86400 * 1)); // 86400 = 1 day
+
+function cx_admin_logged_in() {
+ return isset($_COOKIE['cx_session']) && cx_sessions_active_session_user($_COOKIE['cx_session'], CX_ADMIN_SESSION_LIFETIME) != null;
+}
+
+function cx_admin_login($username, $password) {
+ $password_hash = cx_users_hash_password_for_user($username, $password);
+
+ $user = cx_users_find_user(name: $username, password_hash: $password_hash);
+ if ($user == null) {
+ return false;
+ } else {
+ $session_uid = cx_sessions_create_session($user);
+ setcookie('cx_session', $session_uid, time() + CX_ADMIN_SESSION_LIFETIME, "/");
+ return true;
+ }
+}
+
+function cx_admin_logout() {
+ if (isset($_COOKIE['cx_session'])) {
+ cx_sessions_close_session($_COOKIE['cx_session']);
+ }
+ setcookie('cx_session', null, time(), "/");
+}
--- /dev/null
+<?php
+
+function _cx_db() {
+ static $db = new SQLite3(CX_DATABASE_FILE);
+ return $db;
+}
+
+function _cx_db_sql_exec($sql, ...$args) {
+ $db = _cx_db();
+ $statement = $db->prepare($sql);
+
+ foreach ($args as $i => $arg) {
+ $idx = $i + 1;
+ $statement->bindValue($idx, $arg);
+ }
+
+ return $statement->execute();
+}
+
+function cx_db_exec($sql, ...$args) {
+ _cx_db_sql_exec($sql, ...$args);
+ return _cx_db()->lastInsertRowID();
+}
+
+function cx_db_query($sql, ...$args) {
+ $result_set = _cx_db_sql_exec($sql, ...$args);
+ while ($result = $result_set->fetchArray()) {
+ yield $result;
+ }
+}
--- /dev/null
+<?php
+
+function cx_form_input_sanitized($name) {
+ if (array_key_exists($name, $_POST)) {
+ $insecure_input = $_POST[$name];
+ $tagless_input = strip_tags($insecure_input);
+ return $tagless_input;
+ } else {
+ return null;
+ }
+}
+
+function cx_form_input_sanitized_date_time($name) {
+ $sanitised_string = cx_form_input_sanitized($name);
+
+ if ($sanitised_string != null) {
+ $date = date_parse_from_format('Y-m-d H:i:s', $sanitised_string);
+ if ($date['error_count'] > 0) $date = date_parse_from_format('Y-m-d', $sanitised_string);
+ if ($date['error_count'] == 0) {
+ $timestamp = mktime($date['hour'], $date['minute'], $date['second'], $date['month'], $date['day'], $date['year']);
+ if ($timestamp != false) {
+ return $timestamp;
+ }
+ }
+ }
+
+ return null;
+}
--- /dev/null
+<?php
+
+function cx_http_redirect($url) {
+ header('Location: ' . $url, true, 302);
+}
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+cx_require('lib', 'user_data.php');
+
+
+class Image {
+ public $id;
+ public $uid;
+ public $alt_text;
+ public $url;
+
+ public function __construct($dict) {
+ $this->id = $dict['image_id']; // FIXME, hide when not used?
+ $this->uid = $dict['image_uid'];
+ $this->alt_text = $dict['image_alt_text'];
+ $this->url = $this->uid . '.' . $dict['image_type'];
+ }
+}
+
+function cx_images_add_image($site_id, $alt_text, $image_path, $image_original_filename) {
+ $path_parts = pathinfo($image_original_filename);
+
+ $uid = hash_file("sha256", $image_path);
+
+ $target_name = $uid . "." . $path_parts['extension'];
+ $path = cx_user_data_path('images', $target_name);
+
+ move_uploaded_file($image_path, $path);
+
+ $creation_time = $update_time = time();
+
+ $sql = 'INSERT INTO images (
+ image_site_id,
+ image_creation_time,
+ image_update_time,
+ image_uid,
+ image_type,
+ image_alt_text)
+ VALUES (?, ?, ?, ?, ?, ?);';
+ cx_db_exec($sql, $site_id, $creation_time, $update_time, $uid, $path_parts['extension'], $alt_text);
+}
+
+function cx_images_get(int $limit = 0) {
+ $sql = 'SELECT
+ image_id,
+ image_uid,
+ image_type,
+ image_alt_text
+ FROM images
+ ORDER BY image_creation_time DESC';
+
+ if ($limit > 0) {
+ $sql .= ' LIMIT ' . $limit;
+ }
+
+ $sql .= ';';
+
+ foreach (cx_db_query($sql) as $image) {
+ $p = new Image($image);
+ yield $p;
+ }
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE images (
+ image_id INTEGER PRIMARY KEY,
+ image_site_id INTEGER,
+ image_creation_time INTEGER,
+ image_update_time INTEGER,
+ image_uid STRING,
+ image_type STRING,
+ image_alt_text STRING,
+
+ FOREIGN KEY(image_site_id) REFERENCES sites(site_id)
+ );');
+
+ mkdir(cx_user_data_path('images'), recursive: true);
+});
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+cx_require('third_party', 'parsedown', 'Parsedown.php');
+
+function mk_markdown($markdown) {
+ static $Parsedown = new Parsedown();
+
+ return $Parsedown->text($markdown);
+}
+
+class Post {
+ public $id;
+ public $title;
+ public $slug;
+ public $date;
+ public $data;
+ public $html_content;
+ public $html_excerpt;
+
+ public function __construct($dict) {
+ $this->id = $dict['post_id']; // FIXME, hide when not used?
+ $this->title = $dict['post_title'];
+ $this->slug = $dict['post_slug'];
+ $this->date = $dict['post_date'];
+ $this->data = $dict['post_data'];
+ $this->html_content = mk_markdown($this->data);
+ $this->html_excerpt = null;
+
+ // Read more...
+ $segments = explode('---', $this->data, 2);
+ if (count($segments) > 1) {
+ $this->html_excerpt = mk_markdown($segments[0]);
+ }
+ }
+}
+
+function cx_post_make_slug($title) {
+ $alnum_title = preg_replace('/[^A-Za-z0-9 ]?/', '', $title);
+
+ $slug_components = explode(' ', $alnum_title, 10);
+ $slug_components = array_filter($slug_components);
+ $slug_components = array_values($slug_components); // re-index
+
+ $slug = join('-', $slug_components);
+ $slug = strtolower($slug);
+
+ return $slug;
+}
+
+function cx_posts_add_post($site_id, $title, $slug, $date, $data) {
+ $creation_time = $update_time = time();
+
+ if ($slug == null) {
+ $slug = cx_post_make_slug($title);
+ }
+
+ if ($date == null) {
+ $date = $update_time;
+ }
+
+ $sql = 'INSERT INTO posts (
+ post_site_id,
+ post_creation_time,
+ post_update_time,
+ post_slug,
+ post_date,
+ post_is_page,
+ post_title,
+ post_data,
+ post_data_version)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);';
+ cx_db_exec($sql, $site_id, $creation_time, $update_time, $slug, $date, false, $title, $data, 1);
+}
+
+function cx_posts_update_post($post_id, $title, $slug, $date, $data) {
+ $update_time = time();
+
+ if ($slug == null) {
+ $slug = cx_post_make_slug($title);
+ }
+
+ if ($date == null) {
+ $date = $update_time;
+ }
+
+ $sql = 'UPDATE posts
+ SET post_update_time = ?,
+ post_slug = ?,
+ post_date = ?,
+ post_title = ?,
+ post_data = ?
+ WHERE post_id == ?;';
+ //LIMIT 1;';
+ cx_db_exec($sql, $update_time, $slug, $date , $title, $data, $post_id);
+}
+
+function cx_posts_delete_post($post_id) {
+ $sql = 'DELETE FROM posts
+ WHERE post_id == ?;';
+ //LIMIT 1;';
+ cx_db_exec($sql, $post_id);
+}
+
+function cx_posts_get(int $limit = 0) {
+ $sql = 'SELECT
+ post_id,
+ post_slug,
+ post_date,
+ post_title,
+ post_data
+ FROM posts
+ WHERE post_is_page==FALSE
+ ORDER BY post_date DESC';
+
+ if ($limit > 0) {
+ $sql .= ' LIMIT ' . $limit;
+ }
+
+ $sql .= ';';
+
+ foreach (cx_db_query($sql) as $post) {
+ $p = new Post($post);
+ yield $p;
+ }
+}
+
+function cx_posts_find_post($post_id) {
+ $sql = 'SELECT
+ post_id,
+ post_slug,
+ post_date,
+ post_title,
+ post_data
+ FROM posts
+ WHERE post_is_page == FALSE
+ AND post_id == ?
+ LIMIT 1;';
+
+ foreach (cx_db_query($sql, $post_id) as $post) {
+ $p = new Post($post);
+ return $p;
+ }
+
+ return null;
+}
+
+function cx_posts_find_post_id($post_slug) {
+ $sql = 'SELECT
+ post_id
+ FROM posts
+ WHERE post_slug == ?
+ LIMIT 1;';
+
+ foreach (cx_db_query($sql, $post_slug) as $post) {
+ return $post['post_id'];
+ }
+
+ return null;
+}
+
+function cx_pages_get() {
+ $sql = 'SELECT
+ post_id,
+ post_slug,
+ post_date,
+ post_title,
+ post_data
+ FROM posts
+ WHERE post_is_page == TRUE
+ ORDER BY post_creation_time DESC;';
+
+ foreach (cx_db_query($sql) as $post) {
+ $p = new Post($post);
+ yield $p;
+ }
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE posts (
+ post_id INTEGER PRIMARY KEY,
+ post_site_id INTEGER,
+ post_creation_time INTEGER,
+ post_update_time INTEGER,
+ post_slug STRING,
+ post_date INTEGER,
+ post_is_page BOOLEAN,
+ post_title STRING,
+ post_data BLOB,
+ post_data_version INTEGER,
+
+ FOREIGN KEY(post_site_id) REFERENCES sites(site_id)
+ );');
+});
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+
+function cx_sessions_create_session($user) {
+ $uid = sha1(random_bytes(100));
+ $update_time = $creation_time = time();
+
+ $sql = 'INSERT INTO sessions (
+ session_uid,
+ session_user_id,
+ session_creation_time,
+ session_update_time,
+ session_closed_time)
+ VALUES (?, ?, ?, ?, ?);';
+ cx_db_exec($sql, $uid, $user, $creation_time, $update_time, -1);
+
+ return $uid;
+}
+
+function cx_sessions_close_session($uid) {
+ $close_time = time();
+
+ $sql = 'UPDATE sessions
+ SET session_closed_time = ?
+ WHERE session_uid == ?;';
+ //LIMIT 1;';
+
+ cx_db_exec($sql, $close_time, $uid);
+}
+
+function cx_sessions_active_session_user($uid, $lifetime) {
+ $sql = 'SELECT
+ session_user_id
+ FROM sessions
+ WHERE session_uid == ?
+ AND session_update_time >= ?
+ AND session_closed_time == -1
+ LIMIT 1;';
+
+ $update_time = time() - $lifetime;
+
+ foreach (cx_db_query($sql, $uid, $update_time) as $session) {
+ return $session['session_user_id'];
+ }
+
+ return null;
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE sessions (
+ session_id INTEGER PRIMARY KEY,
+ session_uid STRING,
+ session_user_id INTEGER,
+ session_creation_time INTEGER,
+ session_update_time INTEGER,
+ session_closed_time INTEGER,
+
+ FOREIGN KEY(session_user_id) REFERENCES users(user_id))');
+});
+
+
--- /dev/null
+<?php
+
+$cx_setup_functions = [];
+
+function cx_setup_register(int $version, $function) {
+ global $cx_setup_functions;
+
+ if (isset($version, $cx_setup_functions) == false) {
+ $cx_setup_functions[$version] = [];
+ }
+
+ $cx_setup_functions[$version][] = $function;
+}
+
+function cx_setup_required() {
+ return file_exists(CX_DATABASE_FILE) == false;
+}
+
+function cx_setup_run() {
+ global $cx_setup_functions;
+ foreach ($cx_setup_functions as $version => $functions) {
+ foreach ($functions as $function) {
+ $function();
+ }
+ }
+}
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+
+function _cx_default_site_details(...$args) {
+ $sql = 'SELECT '. join(',', $args) .'
+ FROM sites
+ LIMIT 1;';
+
+ foreach (cx_db_query($sql) as $site) {
+ return $site;
+ }
+}
+
+function cx_sites_add_site($url, $title, $byline, $copyright) {
+ $sql = 'INSERT INTO sites (
+ site_url,
+ site_title,
+ site_byline,
+ site_copyright
+ )
+ VALUES (?, ?, ?, ?);';
+ $site_id = cx_db_exec($sql, $url, $title, $byline, $copyright);
+ return $site_id;
+}
+
+function cx_sites_site_add_user($site_id, $user_id, $owner = false) {
+ $sql = 'INSERT INTO site_authorship (
+ site_authorship_site_id,
+ site_authorship_user_id,
+ site_authorship_owner
+ )
+ VALUES (?, ?, ?);';
+ cx_db_exec($sql, $site_id, $user_id, $owner);
+}
+
+function cx_site_name() {
+ $details = _cx_default_site_details('site_title');
+ return $details['site_title'];
+}
+
+function cx_site_byline() {
+ $details = _cx_default_site_details('site_byline');
+ return $details['site_byline'];
+}
+
+function cx_site_author() {
+ $details = _cx_default_site_details('site_copyright');
+ return $details['site_copyright'];
+}
+
+function cx_site_url() {
+ $details = _cx_default_site_details('site_url');
+ return $details['site_url'];
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE sites (
+ site_id INTEGER PRIMARY KEY,
+ site_url STRING,
+ site_title STRING,
+ site_byline STRING,
+ site_copyright STRING
+ );');
+
+ cx_db_exec('CREATE TABLE site_metadata (
+ site_metadata_site_id INTEGER,
+ site_metadata_key STRING,
+ site_metadata_value STRING,
+
+ PRIMARY KEY (site_metadata_site_id, site_metadata_key),
+ FOREIGN KEY (site_metadata_site_id) REFERENCES sites(site_id)
+ );');
+
+ cx_db_exec('CREATE TABLE site_authorship (
+ site_authorship_id INTEGER PRIMARY KEY,
+ site_authorship_site_id INTEGER,
+ site_authorship_user_id INTEGER,
+ site_authorship_owner BOOLEAN,
+
+ FOREIGN KEY(site_authorship_site_id) REFERENCES sites(site_id),
+ FOREIGN KEY(site_authorship_user_id) REFERENCES users(user_id)
+ );');
+});
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+
+function cx_system_get_metadata($key, $default_value = null) {
+
+}
+
+function cx_system_set_metadata($key, $value) {
+
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE system_metadata (
+ system_metadata_key STRING PRIMARY KEY,
+ system_metadata_value STRING
+ );');
+});
--- /dev/null
+<?php
+
+function cx_template_render($class, $name, $variables = []) {
+ global $cx_template_base, $cx_template_content;
+
+ $base_template = null;
+
+ $output = '';
+ while ($name != null) {
+ $segments = array(CX_PATH, 'templates', $class, $name . '.php');
+ $path = join(DIRECTORY_SEPARATOR, $segments);
+
+ $base_template = null;
+
+ $cx_template_base_previous = $cx_template_base;
+ $cx_template_base = function($name) use (&$base_template ) {
+ $base_template = $name;
+ };
+
+ $cx_template_content_previous = $cx_template_content;
+ $cx_template_content = function() use ($output) {
+ return $output;
+ };
+
+ cx_require('lib', 'url.php'); // For templates
+
+ extract($variables);
+ ob_start();
+ include($path);
+ $output = ob_get_contents();
+ ob_end_clean();
+
+ $cx_template_base = $cx_template_base_previous;
+ $cx_template_content = $cx_template_content_previous;
+
+ $name = $base_template;
+ }
+
+ return $output;
+}
+
+function cx_template_base($name) {
+ global $cx_template_base;
+ $cx_template_base($name);
+}
+
+function cx_template_content() {
+ global $cx_template_content;
+ return $cx_template_content();
+}
--- /dev/null
+<?php
+
+cx_require('lib', 'site.php');
+
+function cx_url($path) {
+ return $path;
+}
+
+function cx_url_admin($path) {
+ return cx_url('/cx' . $path);
+}
+
+function cx_url_site($path) {
+ return cx_site_url() . cx_url($path);
+}
--- /dev/null
+<?php
+
+cx_require('lib', 'setup.php');
+
+function cx_user_data_find_all($type) {
+ $data_directory = join(DIRECTORY_SEPARATOR, array(CX_PUBLIC_USER_DATA_PATH, $type));
+
+ if (file_exists($data_directory)) {
+ foreach (scandir($data_directory) as $path) {
+ yield $path;
+ }
+ }
+}
+
+function cx_user_data_path($type, $filename = null) {
+ $path = join(DIRECTORY_SEPARATOR, array(CX_PUBLIC_USER_DATA_PATH, $type));
+ if ($filename != null) $path = join(DIRECTORY_SEPARATOR, array($path, $filename));
+ return $path;
+}
+
+// define('CX_USER_DATA_PATH', $data_folder_path);
+// define('CX_PUBLIC_USER_DATA_PATH', $public_data_folder_path);
+
+//cx_user_data_register_type($type) {
+//
+//}
+
+//cx_setup_register(2, function() {
+//});
--- /dev/null
+<?php
+
+cx_require('lib', 'db.php');
+cx_require('lib', 'setup.php');
+
+function cx_users_find_user($id = null, $name = null, $password_hash = null) {
+ if ($id != null) {
+ $sql = 'SELECT
+ user_id
+ FROM users
+ WHERE user_id == ?
+ LIMIT 1;';
+
+ foreach (cx_db_query($sql, $id) as $user) {
+ return $user['user_id'];
+ }
+ } else if ($name != null && $password_hash != null) {
+ $sql = 'SELECT
+ user_id
+ FROM users
+ WHERE user_name == ?
+ AND user_password_hash == ?
+ LIMIT 1;';
+
+ foreach (cx_db_query($sql, $name, $password_hash) as $user) {
+ return $user['user_id'];
+ }
+ }
+
+ return null;
+}
+
+function cx_users_hash_password_for_user($username, $password) {
+ $sql = 'SELECT
+ user_salt
+ FROM users
+ WHERE user_name == ?
+ LIMIT 1;';
+
+ $salt = '';
+ foreach (cx_db_query($sql, $username) as $user) {
+ $salt = $user['user_salt'];
+ }
+
+ $password_hash = sha1($salt . $password);
+ return $password_hash;
+}
+
+function cx_users_add_user($name, $password) {
+ $creation_time = time();//
+ $salt = sha1(random_bytes(100));
+ $password_hash = sha1($salt . $password);
+
+ $sql = 'INSERT INTO users (
+ user_creation_time,
+ user_name,
+ user_salt,
+ user_password_hash
+ )
+ VALUES (?, ?, ?, ?);';
+ $user_id = cx_db_exec($sql, $creation_time, $name, $salt, $password_hash);
+ return $user_id;
+}
+
+cx_setup_register(1, function() {
+ cx_db_exec('CREATE TABLE users (
+ user_id INTEGER PRIMARY KEY,
+ user_creation_time INTEGER,
+ user_name STRING,
+ user_salt STRING,
+ user_password_hash STRING
+ );');
+});
--- /dev/null
+<?php cx_template_base('skeleton'); ?>
+
+<nav>
+<ul role="list">
+<li><a href="<?= cx_url_admin('/'); ?>">admin</a></li>
+<li><a href="<?= cx_url_admin('/posts/add/'); ?>">add post/page</a></li>
+<li><a href="<?= cx_url_admin('/images/add/'); ?>">add image</a></li>
+<li><a href="<?= cx_url_admin('/logout/'); ?>">logout</a></li>
+</ul>
+</nav>
+
+<main>
+<?= cx_template_content(); ?>
+</main>
--- /dev/null
+<?php cx_template_base('base'); ?>
+
+<form action="<?= cx_url_admin('/images/update?id=' . $image_id); ?>" method="post" enctype="multipart/form-data">
+
+<p>alt-text: <input name="image_alt_text" type="text" value="<?= $image_alt_text ?>"></p>
+
+<?php if ($image_id == 0): ?>
+<p>file: <input name="image_file" type="file"></p>
+<?php else: ?>
+<p></p>
+<?php endif; ?>
+
+<p><input type="submit" value="update"></p>
+</form>
--- /dev/null
+<?php cx_template_base('skeleton'); ?>
+<form action="<?= cx_url_admin('/login/'); ?>" method="post">
+<input name="id" type="text">
+<input name="password" type="password">
+<input type="submit">
+</form>
--- /dev/null
+<?php cx_template_base('base'); ?>
+
+<h2>Posts</h2>
+<ul role="list">
+<?php foreach (cx_posts_get() as $post): ?>
+<li><a href="<?= cx_url_admin('/posts/edit?id=' . $post->id); ?>"><?= $post->title ?></a> <a href="<?= cx_url_admin('/posts/delete?id=' . $post->id); ?>">🚮</a></li>
+<?php endforeach; ?>
+</ul>
+
+<h2>Pages</h2>
+<ul role="list">
+<?php foreach (cx_pages_get() as $post): ?>
+<li><a href="<?= cx_url_admin('/posts/edit?id=' . $post->id); ?>"><?= $post->title ?></a> <a href="<?= cx_url_admin('/posts/delete?id=' . $post->id); ?>">🚮</a></li>
+<?php endforeach; ?>
+</ul>
+
+<h2>Images</h2>
+<ul role="list">
+<?php foreach (cx_images_get() as $image): ?>
+<li><a href="<?= cx_url_admin('/images/edit?id=' . $image->id); ?>"><?= $image->url ?></a> <a href="<?= cx_url_admin('/images/delete?id=' . $image->id); ?>">🚮</a></li>
+<?php endforeach; ?>
+</ul>
--- /dev/null
+<?php cx_template_base('base'); ?>
+
+<form action="<?= cx_url_admin('/posts/update?id=' . $post_id); ?>" method="post">
+
+<p>title: <input name="post_title" type="text" value="<?= $post_title ?>"></p>
+<p>slug: <input name="post_slug" type="text" value="<?= $post_slug ?>"></p>
+<p>date: <input name="post_date" type="text" value="<?= $post_date ? date('Y-m-d H:i:s', $post_date) : "" ?>"></p>
+<p><textarea name="post_data" cols="60" rows="40"><?= $post_data ?></textarea></p>
+
+<p><input type="submit" value="update"></p>
+</form>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>CX Admin Panel</title>
+ <link rel="stylesheet" href="<?= cx_url('/design/css/style.css') ?>">
+</head>
+
+<body>
+<?= cx_template_content(); ?>
+</body>
+</html>
--- /dev/null
+<?= '<?xml version="1.0" encoding="utf-8"?>'."\n" /* So some installs of PHP don't get confused... */ ?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id><?= cx_url_site('/'); ?></id>
+
+ <title><?= cx_site_name(); ?></title>
+ <subtitle><?= cx_site_byline(); ?></subtitle>
+
+ <?php foreach (cx_posts_get(limit: 1) as $post): ?>
+ <updated><?= date(DATE_RFC3339, $post->date) ?></updated>
+ <?php endforeach; ?>
+
+ <link href="<?= cx_url_site('/feed/'); ?>" rel="self" type="application/atom+xml" />
+ <link href="<?= cx_url('/'); ?>" rel="alternate" type="text/html" />
+
+ <author>
+ <name><?= cx_site_author() ?></name>
+ </author>
+
+<?php foreach (cx_posts_get() as $post): ?>
+<?php $post_permalink = '/' . date('Y', $post->date) . '/' . date('m', $post->date) . '/' . $post->slug; ?>
+ <entry>
+ <id><?= cx_url_site($post_permalink); ?></id>
+ <title><?= $post->title ?></title>
+ <updated><?= date(DATE_RFC3339, $post->date) ?></updated>
+ <link href="<?= cx_url($post_permalink) ?>" rel="alternate" type="text/html" />
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <?= $post->html_content ?>
+ </div>
+ </content>
+ </entry>
+<?php endforeach; ?>
+
+</feed>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title><?= cx_site_name(); ?></title>
+ <link rel="stylesheet" href="<?= cx_url('/design/css/style.css') ?>">
+ <link rel="alternate" type="application/rss+xml" title="bts.cx" href="<?= cx_url('/feed/') ?>" />
+</head>
+
+<body>
+<header>
+ <h1><?= cx_site_name(); ?></h1>
+</header>
+
+<nav>
+
+<ul role="list">
+<li><a href="<?= cx_url('/'); ?>">Home</a></li>
+
+<?php foreach (cx_pages_get() as $page): ?>
+<li><a href="<?= cx_url($page->url); ?>"><?= $page->title ?></a></li>
+<?php endforeach; ?>
+</ul>
+</nav>
+
+<?= cx_template_content(); ?>
+
+<footer>
+
+<div>
+<p>© <?= date('Y'); ?> <?= cx_site_name(); ?> · <?= cx_site_author(); ?></p>
+</div>
+
+</footer>
+
+</body>
+</html>
--- /dev/null
+<?php cx_template_base('base'); ?>
+<main>
+<?php foreach (cx_posts_get() as $post): ?>
+<?php $post_permalink = '/' . date('Y', $post->date) . '/' . date('m', $post->date) . '/' . $post->slug; ?>
+ <article>
+ <h1><a href="<?= cx_url($post_permalink) ?>"><?= $post->title ?></a></h1>
+ <p class="updated"><?= date('l, F jS, Y',$post->date) ?></p>
+ <?php if ($post->html_excerpt): ?>
+ <?= $post->html_excerpt ?>
+ <p><a href="<?= cx_url($post_permalink) ?>">Read more...</a></p>
+ <?php else: ?>
+ <?= $post->html_content ?>
+ <?php endif; ?>
+ </article>
+<?php endforeach; ?>
+</main>
--- /dev/null
+<?php cx_template_base('base'); ?>
+<main>
+<?php $post = cx_posts_find_post($post_id); ?>
+<?php if ($post): ?>
+<?php $post_permalink = '/' . date('Y', $post->date) . '/' . date('m', $post->date) . '/' . $post->slug; ?>
+ <article>
+ <h1><a href="<?= cx_url($post_permalink) ?>"><?= $post->title ?></a></h1>
+ <p class="updated"><?= date('l, F jS, Y',$post->date) ?></p>
+ <?= $post->html_content ?>
+ </article>
+<?php else: ?>
+ <p>Nothing found.</p>
+<?php endif; ?>
+
+</main>
--- /dev/null
+The MIT License (MIT)
+
+Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
+
+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.
--- /dev/null
+<?php
+
+#
+#
+# Parsedown
+# http://parsedown.org
+#
+# (c) Emanuil Rusev
+# http://erusev.com
+#
+# For the full license information, view the LICENSE file that was distributed
+# with this source code.
+#
+#
+
+class Parsedown
+{
+ # ~
+
+ const version = '1.7.4';
+
+ # ~
+
+ function text($text)
+ {
+ # make sure no definitions are set
+ $this->DefinitionData = array();
+
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ # iterate through lines to identify blocks
+ $markup = $this->lines($lines);
+
+ # trim line breaks
+ $markup = trim($markup, "\n");
+
+ return $markup;
+ }
+
+ #
+ # Setters
+ #
+
+ function setBreaksEnabled($breaksEnabled)
+ {
+ $this->breaksEnabled = $breaksEnabled;
+
+ return $this;
+ }
+
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ function setSafeMode($safeMode)
+ {
+ $this->safeMode = (bool) $safeMode;
+
+ return $this;
+ }
+
+ protected $safeMode;
+
+ protected $safeLinksWhitelist = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'ftps://',
+ 'mailto:',
+ 'data:image/png;base64,',
+ 'data:image/gif;base64,',
+ 'data:image/jpeg;base64,',
+ 'irc:',
+ 'ircs:',
+ 'git:',
+ 'ssh:',
+ 'news:',
+ 'steam:',
+ );
+
+ #
+ # Lines
+ #
+
+ protected $BlockTypes = array(
+ '#' => array('Header'),
+ '*' => array('Rule', 'List'),
+ '+' => array('List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+ '0' => array('List'),
+ '1' => array('List'),
+ '2' => array('List'),
+ '3' => array('List'),
+ '4' => array('List'),
+ '5' => array('List'),
+ '6' => array('List'),
+ '7' => array('List'),
+ '8' => array('List'),
+ '9' => array('List'),
+ ':' => array('Table'),
+ '<' => array('Comment', 'Markup'),
+ '=' => array('SetextHeader'),
+ '>' => array('Quote'),
+ '[' => array('Reference'),
+ '_' => array('Rule'),
+ '`' => array('FencedCode'),
+ '|' => array('Table'),
+ '~' => array('FencedCode'),
+ );
+
+ # ~
+
+ protected $unmarkedBlockTypes = array(
+ 'Code',
+ );
+
+ #
+ # Blocks
+ #
+
+ protected function lines(array $lines)
+ {
+ $CurrentBlock = null;
+
+ foreach ($lines as $line)
+ {
+ if (chop($line) === '')
+ {
+ if (isset($CurrentBlock))
+ {
+ $CurrentBlock['interrupted'] = true;
+ }
+
+ continue;
+ }
+
+ if (strpos($line, "\t") !== false)
+ {
+ $parts = explode("\t", $line);
+
+ $line = $parts[0];
+
+ unset($parts[0]);
+
+ foreach ($parts as $part)
+ {
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+
+ $line .= str_repeat(' ', $shortage);
+ $line .= $part;
+ }
+ }
+
+ $indent = 0;
+
+ while (isset($line[$indent]) and $line[$indent] === ' ')
+ {
+ $indent ++;
+ }
+
+ $text = $indent > 0 ? substr($line, $indent) : $line;
+
+ # ~
+
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']))
+ {
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+
+ continue;
+ }
+ else
+ {
+ if ($this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+ }
+ }
+
+ # ~
+
+ $marker = $text[0];
+
+ # ~
+
+ $blockTypes = $this->unmarkedBlockTypes;
+
+ if (isset($this->BlockTypes[$marker]))
+ {
+ foreach ($this->BlockTypes[$marker] as $blockType)
+ {
+ $blockTypes []= $blockType;
+ }
+ }
+
+ #
+ # ~
+
+ foreach ($blockTypes as $blockType)
+ {
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $Block['type'] = $blockType;
+
+ if ( ! isset($Block['identified']))
+ {
+ $Blocks []= $CurrentBlock;
+
+ $Block['identified'] = true;
+ }
+
+ if ($this->isBlockContinuable($blockType))
+ {
+ $Block['continuable'] = true;
+ }
+
+ $CurrentBlock = $Block;
+
+ continue 2;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
+ {
+ $CurrentBlock['element']['text'] .= "\n".$text;
+ }
+ else
+ {
+ $Blocks []= $CurrentBlock;
+
+ $CurrentBlock = $this->paragraph($Line);
+
+ $CurrentBlock['identified'] = true;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+
+ # ~
+
+ $Blocks []= $CurrentBlock;
+
+ unset($Blocks[0]);
+
+ # ~
+
+ $markup = '';
+
+ foreach ($Blocks as $Block)
+ {
+ if (isset($Block['hidden']))
+ {
+ continue;
+ }
+
+ $markup .= "\n";
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+ }
+
+ $markup .= "\n";
+
+ # ~
+
+ return $markup;
+ }
+
+ protected function isBlockContinuable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Continue');
+ }
+
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Complete');
+ }
+
+ #
+ # Code
+
+ protected function blockCode($Line, $Block = null)
+ {
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] >= 4)
+ {
+ $text = substr($Line['body'], 4);
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeContinue($Line, $Block)
+ {
+ if ($Line['indent'] >= 4)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text']['text'] .= "\n";
+
+ $text = substr($Line['body'], 4);
+
+ $Block['element']['text']['text'] .= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Comment
+
+ protected function blockComment($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
+ {
+ $Block = array(
+ 'markup' => $Line['body'],
+ );
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockCommentContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ $Block['markup'] .= "\n" . $Line['body'];
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+
+ #
+ # Fenced Code
+
+ protected function blockFencedCode($Line)
+ {
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches))
+ {
+ $Element = array(
+ 'name' => 'code',
+ 'text' => '',
+ );
+
+ if (isset($matches[1]))
+ {
+ /**
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+ * Every HTML element may have a class attribute specified.
+ * The attribute, if specified, must have a value that is a set
+ * of space-separated tokens representing the various classes
+ * that the element belongs to.
+ * [...]
+ * The space characters, for the purposes of this specification,
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+ * U+000D CARRIAGE RETURN (CR).
+ */
+ $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
+
+ $class = 'language-'.$language;
+
+ $Element['attributes'] = array(
+ 'class' => $class,
+ );
+ }
+
+ $Block = array(
+ 'char' => $Line['text'][0],
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => $Element,
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockFencedCodeContinue($Line, $Block)
+ {
+ if (isset($Block['complete']))
+ {
+ return;
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
+ {
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
+
+ $Block['complete'] = true;
+
+ return $Block;
+ }
+
+ $Block['element']['text']['text'] .= "\n".$Line['body'];
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ if (isset($Line['text'][1]))
+ {
+ $level = 1;
+
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
+ {
+ $level ++;
+ }
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '# ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . min(6, $level),
+ 'text' => $text,
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # List
+
+ protected function blockList($Line)
+ {
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
+
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'indent' => $Line['indent'],
+ 'pattern' => $pattern,
+ 'element' => array(
+ 'name' => $name,
+ 'handler' => 'elements',
+ ),
+ );
+
+ if($name === 'ol')
+ {
+ $listStart = stristr($matches[0], '.', true);
+
+ if($listStart !== '1')
+ {
+ $Block['element']['attributes'] = array('start' => $listStart);
+ }
+ }
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $matches[2],
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+ }
+
+ protected function blockListContinue($Line, array $Block)
+ {
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['text'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ unset($Block['li']);
+
+ $text = isset($matches[1]) ? $matches[1] : '';
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $text,
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ return $Block;
+ }
+
+ if ($Line['indent'] > 0)
+ {
+ $Block['li']['text'] []= '';
+
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ unset($Block['interrupted']);
+
+ return $Block;
+ }
+ }
+
+ protected function blockListComplete(array $Block)
+ {
+ if (isset($Block['loose']))
+ {
+ foreach ($Block['element']['text'] as &$li)
+ {
+ if (end($li['text']) !== '')
+ {
+ $li['text'] []= '';
+ }
+ }
+ }
+
+ return $Block;
+ }
+
+ #
+ # Quote
+
+ protected function blockQuote($Line)
+ {
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'blockquote',
+ 'handler' => 'lines',
+ 'text' => (array) $matches[1],
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockQuoteContinue($Line, array $Block)
+ {
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= '';
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text'] []= $matches[1];
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= $Line['text'];
+
+ return $Block;
+ }
+ }
+
+ #
+ # Rule
+
+ protected function blockRule($Line)
+ {
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'hr'
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Setext
+
+ protected function blockSetextHeader($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (chop($Line['text'], $Line['text'][0]) === '')
+ {
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+ return $Block;
+ }
+ }
+
+ #
+ # Markup
+
+ protected function blockMarkup($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
+ {
+ return;
+ }
+
+ $Block = array(
+ 'name' => $matches[1],
+ 'depth' => 0,
+ 'markup' => $Line['text'],
+ );
+
+ $length = strlen($matches[0]);
+
+ $remainder = substr($Line['text'], $length);
+
+ if (trim($remainder) === '')
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ $Block['closed'] = true;
+
+ $Block['void'] = true;
+ }
+ }
+ else
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ return;
+ }
+
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockMarkupContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
+ {
+ $Block['depth'] ++;
+ }
+
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
+ {
+ if ($Block['depth'] > 0)
+ {
+ $Block['depth'] --;
+ }
+ else
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['markup'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['markup'] .= "\n".$Line['body'];
+
+ return $Block;
+ }
+
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
+ {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => null,
+ );
+
+ if (isset($matches[3]))
+ {
+ $Data['title'] = $matches[3];
+ }
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'hidden' => true,
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Table
+
+ protected function blockTable($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
+ {
+ $alignments = array();
+
+ $divider = $Line['text'];
+
+ $divider = trim($divider);
+ $divider = trim($divider, '|');
+
+ $dividerCells = explode('|', $divider);
+
+ foreach ($dividerCells as $dividerCell)
+ {
+ $dividerCell = trim($dividerCell);
+
+ if ($dividerCell === '')
+ {
+ continue;
+ }
+
+ $alignment = null;
+
+ if ($dividerCell[0] === ':')
+ {
+ $alignment = 'left';
+ }
+
+ if (substr($dividerCell, - 1) === ':')
+ {
+ $alignment = $alignment === 'left' ? 'center' : 'right';
+ }
+
+ $alignments []= $alignment;
+ }
+
+ # ~
+
+ $HeaderElements = array();
+
+ $header = $Block['element']['text'];
+
+ $header = trim($header);
+ $header = trim($header, '|');
+
+ $headerCells = explode('|', $header);
+
+ foreach ($headerCells as $index => $headerCell)
+ {
+ $headerCell = trim($headerCell);
+
+ $HeaderElement = array(
+ 'name' => 'th',
+ 'text' => $headerCell,
+ 'handler' => 'line',
+ );
+
+ if (isset($alignments[$index]))
+ {
+ $alignment = $alignments[$index];
+
+ $HeaderElement['attributes'] = array(
+ 'style' => 'text-align: '.$alignment.';',
+ );
+ }
+
+ $HeaderElements []= $HeaderElement;
+ }
+
+ # ~
+
+ $Block = array(
+ 'alignments' => $alignments,
+ 'identified' => true,
+ 'element' => array(
+ 'name' => 'table',
+ 'handler' => 'elements',
+ ),
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'thead',
+ 'handler' => 'elements',
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'tbody',
+ 'handler' => 'elements',
+ 'text' => array(),
+ );
+
+ $Block['element']['text'][0]['text'] []= array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $HeaderElements,
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockTableContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
+ {
+ $Elements = array();
+
+ $row = $Line['text'];
+
+ $row = trim($row);
+ $row = trim($row, '|');
+
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
+
+ foreach ($matches[0] as $index => $cell)
+ {
+ $cell = trim($cell);
+
+ $Element = array(
+ 'name' => 'td',
+ 'handler' => 'line',
+ 'text' => $cell,
+ );
+
+ if (isset($Block['alignments'][$index]))
+ {
+ $Element['attributes'] = array(
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
+ );
+ }
+
+ $Elements []= $Element;
+ }
+
+ $Element = array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $Elements,
+ );
+
+ $Block['element']['text'][1]['text'] []= $Element;
+
+ return $Block;
+ }
+ }
+
+ #
+ # ~
+ #
+
+ protected function paragraph($Line)
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'p',
+ 'text' => $Line['text'],
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+
+ #
+ # Inline Elements
+ #
+
+ protected $InlineTypes = array(
+ '"' => array('SpecialCharacter'),
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
+ '*' => array('Emphasis'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
+ '>' => array('SpecialCharacter'),
+ '[' => array('Link'),
+ '_' => array('Emphasis'),
+ '`' => array('Code'),
+ '~' => array('Strikethrough'),
+ '\\' => array('EscapeSequence'),
+ );
+
+ # ~
+
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
+
+ #
+ # ~
+ #
+
+ public function line($text, $nonNestables=array())
+ {
+ $markup = '';
+
+ # $excerpt is based on the first occurrence of a marker
+
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+ {
+ $marker = $excerpt[0];
+
+ $markerPosition = strpos($text, $marker);
+
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+ foreach ($this->InlineTypes[$marker] as $inlineType)
+ {
+ # check to see if the current inline type is nestable in the current context
+
+ if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables))
+ {
+ continue;
+ }
+
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
+
+ if ( ! isset($Inline))
+ {
+ continue;
+ }
+
+ # makes sure that the inline belongs to "our" marker
+
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+ {
+ continue;
+ }
+
+ # sets a default inline position
+
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
+
+ # cause the new element to 'inherit' our non nestables
+
+ foreach ($nonNestables as $non_nestable)
+ {
+ $Inline['element']['nonNestables'][] = $non_nestable;
+ }
+
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
+
+ # compile the unmarked text
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ # compile the inline
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+ continue 2;
+ }
+
+ # the marker does not belong to an inline
+
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ $text = substr($text, $markerPosition + 1);
+ }
+
+ $markup .= $this->unmarkedText($text);
+
+ return $markup;
+ }
+
+ #
+ # ~
+ #
+
+ protected function inlineCode($Excerpt)
+ {
+ $marker = $Excerpt['text'][0];
+
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
+ {
+ $text = $matches[2];
+ $text = preg_replace("/[ ]*\n/", ' ', $text);
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmailTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ if ( ! isset($matches[2]))
+ {
+ $url = 'mailto:' . $url;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmphasis($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ $marker = $Excerpt['text'][0];
+
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'strong';
+ }
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'em';
+ }
+ else
+ {
+ return;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => $emphasis,
+ 'handler' => 'line',
+ 'text' => $matches[1],
+ ),
+ );
+ }
+
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'markup' => $Excerpt['text'][1],
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['text'],
+ ),
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => 'line',
+ 'nonNestables' => array('Url', 'Link'),
+ 'text' => null,
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+ {
+ $Element['text'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['text']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
+ {
+ return array(
+ 'markup' => '&',
+ 'extent' => 1,
+ );
+ }
+
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
+
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
+ {
+ return array(
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
+ 'extent' => 1,
+ );
+ }
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'text' => $matches[1],
+ 'handler' => 'line',
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
+ {
+ $url = $matches[0][0];
+
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ # ~
+
+ protected function unmarkedText($text)
+ {
+ if ($this->breaksEnabled)
+ {
+ $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
+ }
+ else
+ {
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
+ $text = str_replace(" \n", "\n", $text);
+ }
+
+ return $text;
+ }
+
+ #
+ # Handlers
+ #
+
+ protected function element(array $Element)
+ {
+ if ($this->safeMode)
+ {
+ $Element = $this->sanitiseElement($Element);
+ }
+
+ $markup = '<'.$Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= ' '.$name.'="'.self::escape($value).'"';
+ }
+ }
+
+ $permitRawHtml = false;
+
+ if (isset($Element['text']))
+ {
+ $text = $Element['text'];
+ }
+ // very strongly consider an alternative if you're writing an
+ // extension
+ elseif (isset($Element['rawHtml']))
+ {
+ $text = $Element['rawHtml'];
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+ }
+
+ if (isset($text))
+ {
+ $markup .= '>';
+
+ if (!isset($Element['nonNestables']))
+ {
+ $Element['nonNestables'] = array();
+ }
+
+ if (isset($Element['handler']))
+ {
+ $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
+ }
+ elseif (!$permitRawHtml)
+ {
+ $markup .= self::escape($text, true);
+ }
+ else
+ {
+ $markup .= $text;
+ }
+
+ $markup .= '</'.$Element['name'].'>';
+ }
+ else
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ foreach ($Elements as $Element)
+ {
+ $markup .= "\n" . $this->element($Element);
+ }
+
+ $markup .= "\n";
+
+ return $markup;
+ }
+
+ # ~
+
+ protected function li($lines)
+ {
+ $markup = $this->lines($lines);
+
+ $trimmedMarkup = trim($markup);
+
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
+ {
+ $markup = $trimmedMarkup;
+ $markup = substr($markup, 3);
+
+ $position = strpos($markup, "</p>");
+
+ $markup = substr_replace($markup, '', $position, 4);
+ }
+
+ return $markup;
+ }
+
+ #
+ # Deprecated Methods
+ #
+
+ function parse($text)
+ {
+ $markup = $this->text($text);
+
+ return $markup;
+ }
+
+ protected function sanitiseElement(array $Element)
+ {
+ static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+ static $safeUrlNameToAtt = array(
+ 'a' => 'href',
+ 'img' => 'src',
+ );
+
+ if (isset($safeUrlNameToAtt[$Element['name']]))
+ {
+ $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+ }
+
+ if ( ! empty($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $att => $val)
+ {
+ # filter out badly parsed attribute
+ if ( ! preg_match($goodAttribute, $att))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ # dump onevent attribute
+ elseif (self::striAtStart($att, 'on'))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ }
+ }
+
+ return $Element;
+ }
+
+ protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+ {
+ foreach ($this->safeLinksWhitelist as $scheme)
+ {
+ if (self::striAtStart($Element['attributes'][$attribute], $scheme))
+ {
+ return $Element;
+ }
+ }
+
+ $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+ return $Element;
+ }
+
+ #
+ # Static Methods
+ #
+
+ protected static function escape($text, $allowQuotes = false)
+ {
+ return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+ }
+
+ protected static function striAtStart($string, $needle)
+ {
+ $len = strlen($needle);
+
+ if ($len > strlen($string))
+ {
+ return false;
+ }
+ else
+ {
+ return strtolower(substr($string, 0, $len)) === strtolower($needle);
+ }
+ }
+
+ static function instance($name = 'default')
+ {
+ if (isset(self::$instances[$name]))
+ {
+ return self::$instances[$name];
+ }
+
+ $instance = new static();
+
+ self::$instances[$name] = $instance;
+
+ return $instance;
+ }
+
+ private static $instances = array();
+
+ #
+ # Fields
+ #
+
+ protected $DefinitionData;
+
+ #
+ # Read-Only
+
+ protected $specialCharacters = array(
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
+ );
+
+ protected $StrongRegex = array(
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
+ );
+
+ protected $EmRegex = array(
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ );
+
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
+
+ protected $voidElements = array(
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+ );
+
+ protected $textLevelElements = array(
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
+ 'q', 'rt', 'ins', 'font', 'strong',
+ 's', 'tt', 'kbd', 'mark',
+ 'u', 'xm', 'sub', 'nobr',
+ 'sup', 'ruby',
+ 'var', 'span',
+ 'wbr', 'time',
+ );
+}
--- /dev/null
+# BEGIN CX
+
+RewriteEngine On
+RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+RewriteBase /
+RewriteRule ^index\.php$ - [L]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule . /index.php [L]
+
+# END CX
\ No newline at end of file
--- /dev/null
+/* Very Smol Reset */
+* {
+ box-sizing: border-box;
+ margin: 0;
+}
+
+@font-face {
+ font-family: Fanwood;
+ src: url(/design/fonts/fanwood/fanwood-webfont.woff);
+}
+
+@font-face {
+ font-family: Fanwood;
+ src: url(/design/fonts/fanwood/fanwood_italic-webfont.woff);
+ font-style: italic;
+}
+
+@font-face {
+ font-family: "Fanwood Text";
+ src: url(/design/fonts/fanwood/fanwood_text-webfont.woff);
+}
+
+@font-face {
+ font-family: "Fanwood Text";
+ src: url(/design/fonts/fanwood/fanwood_text_italic-webfont.woff);
+ font-style: italic;
+}
+
+/* Layout */
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ padding: 5vh clamp(1rem, 5vw, 3rem) 1rem;
+ font-family: Fanwood, serif;
+ font-size: 1.4em;
+ line-height: 1.2em;
+ color: #222;
+}
+
+body > * {
+ --layout-spacing: max(8vh, 3rem);
+ --max-width: 60ch;
+ width: min(100%, var(--max-width));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+header h1 {
+ font-size: 4em;
+}
+
+nav ul {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0;
+ margin-left: -1rem;
+ margin-right: -1rem;
+ vertical-align: bottom;
+}
+
+nav ul li {
+ padding: 0.5rem 1rem;
+}
+
+nav [aria-current="page"] {
+ font-weight: bold;
+}
+
+main,
+main > * + * {
+ margin-top: var(--layout-spacing);
+}
+
+body > footer {
+ margin-top: auto;
+ padding-top: var(--layout-spacing);
+}
+
+body > footer div {
+ border-top: 1px solid #ccc;
+ padding-top: 0.5em;
+ font-size: 0.9rem;
+ color: #767676;
+}
+
+article * + * {
+ margin-top: 1em;
+}
+
+article > h1 {
+ margin-top: 0;
+}
+
+article > h1 a {
+ text-decoration: none;
+ color: #000;
+}
+
+article > h1 + .updated {
+ margin-top: 0;
+}
+
+
+/* Typography */
+:is(h1, h2, h3) {
+ line-height: 1.2;
+}
+
+:is(h1, h2) {
+ max-width: 40ch;
+}
+
+
+
+.updated {
+ font-size: 0.8em;
+ color: #aaa;
+}
+
+:is(h2, h3):not(:first-child) {
+ margin-top: 2em;
+}
+
+a {
+ color: navy;
+ text-underline-offset: 0.08em;
+}
+
+a:focus {
+ outline: 1px solid currentColor;
+ outline-offset: 0.2em;
+}
+
+/* Media */
+article + article {
+ border-top: 1px dashed #ccc;
+ padding-top: var(--layout-spacing);
+}
+
+article img {
+ display: block;
+ width: 100%;
+ min-height: 20rem;
+ max-height: 40vh;
+ object-fit: scale-down; /*cover;*/
+ margin: 2rem auto;
+}
+
+@supports (aspect-ratio: 1) {
+ article img {
+ max-height: unset;
+ aspect-ratio: 3/2;
+ }
+}
+
+/* Supporting Content */
+
+code:not([class*="language"]) {
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+ font-size: 1.75ex;
+ color: #444;
+ background-color: rgba(0, 0, 0, 0.1);
+ padding-right: 0.15em;
+ padding-left: 0.15em;
+}
+
+blockquote {
+ margin: 2rem 0;
+ padding: 0.5em 1rem;
+ border-left: 3px solid rgba(0, 0, 0, 0.35);
+ background-color: rgba(0, 0, 0, 0.05);
+ border-radius: 0 0.25rem 0.25rem 0;
+}
--- /dev/null
+<?php
+
+/*
+ ,-----,--. ,--. ,-----. ,--.
+' .--./\ `.' / | |) /_| |,---. ,---.,--. ,--.,--,--,--.--.,---.
+| | .' \ | .-. | | .-. | .-. | |.'.| ' ,-. | .--| .-. :
+' '--'\/ .'. \ | '--' | ' '-' ' '-' | .'. \ '-' | | \ --.
+ `-----'--' '--' `------'`--'`---'.`- /'--' '--'`--`--`--' `----'
+ `---'
+ */
+
+$main_file = join(DIRECTORY_SEPARATOR, array(__DIR__, "..", "cx", "cx.php"));
+$db_file = join(DIRECTORY_SEPARATOR, array(__DIR__, "..", "db", "btscx.db"));
+$data_folder = join(DIRECTORY_SEPARATOR, array(__DIR__, "..", "data"));
+$public_data_folder = join(DIRECTORY_SEPARATOR, array(__DIR__, "data"));
+require_once($main_file);
+cx($db_file, $data_folder, $public_data_folder);
--- /dev/null
+<?php
+
+define('CX_SETUP_USER', 'admin');
+define('CX_SETUP_PASSWORD', 'password');
+
+define('CX_SETUP_URL', 'https://blog.blog');
+define('CX_SETUP_TITLE', 'Basic Blog');
+define('CX_SETUP_BYLINE', 'Just a basic blog');
+define('CX_SETUP_COPYRIGHT', 'Me');
\ No newline at end of file