Table of Contents
Bliki Plugin
Compatible with DokuWiki
2011-05-25 "Rincewind", "Angua"
Creates a simple blog engine using namespaces to store entries by date (apparently in development at the same time as Esther's!)
This extension has not been updated in over 2 years. It may no longer be maintained or supported and may have compatibility issues.
Similar to blog, blogtng, dokuwikitweet, log
By Beau Lebens and Anthony Caetano.
Description
Bliki allows you to create a simple blog on any page of your wiki by adding a single tag (~~BLIKI~~
) to your page. It uses date-based namespaces to store your entries, and automatically handles paging of posts, a 'new post' link, date headers and post footers.
Version/Requirements
Latest version released 2005-10-16 and developed/tested using the 2005-07-13 version of DokuWiki.
Update: thanks to Taggic, bliki now works with Rincewind and Angua.
Previous Versions
- First release: deep storage only.
Installation
Plugin Source
Create a directory called bliki
inside your lib/plugins/
directory, and then copy the following source into syntax.php
in that directory (lib/plugins/bliki/syntax.php
). Obviously make sure the file is readable by your webserver.
2011-10-31 Taggic → tested following on 2011-05-25a “Rincewind”
- syntax.php
<?php /** * bliki Plugin: Adds a simple blogging engine to your wiki * * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Beau Lebens <beau@dentedreality.com.au> * @author Anthony Caetano <Anthony.Caetano@Sanlam.co.za> * 2011-10-31 modified by Taggic to get is work with current dokuwiki (Rincewind) */ if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'syntax.php'); /** * All DokuWiki plugins to extend the parser/rendering mechanism * need to inherit from this class */ class syntax_plugin_bliki extends DokuWiki_Syntax_Plugin { /** * return some info */ function getInfo(){ return array( 'author' => 'Beau Lebens', 'email' => 'beau@dentedreality.com.au', 'date' => '2005-09-06', 'name' => 'Bliki: The Wiki Blog', 'desc' => 'Adds basic blogging functionality to any page of your wiki.', 'url' => 'http://www.dokuwiki.org/plugin:bliki', ); } /** * What kind of syntax are we? */ function getType(){ return 'substition'; } /** * What kind of syntax do we allow (optional) */ function getAllowedTypes() { return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); } /** * What about paragraphs? (optional) */ function getPType(){ return 'block'; } /** * Where to sort in? */ function getSort(){ return 400; } /** * Connect pattern to lexer */ function connectTo($mode) { $this->Lexer->addSpecialPattern('~~BLIKI~~', $mode, 'plugin_bliki'); } /** * Handle the match */ function handle($match, $state, $pos, &$handler){ return array(); } /** * @return Array * @param String $ID * @param Int $num * @param Int $offset * @desc Finds the full pathnames of the most recent $num posts, starting at the optional within the $ID blog/namespace. */ function getPosts($ID, $num, $offset = 0) { global $conf; $recents = array(); $counter = 0; // fully-qaulify the ID that we're working with (to dig into the namespace) $fp = wikiFN($ID); $ID = substr($fp, 0, strrpos($fp, '.')); // Only do it if the namespace exists if (is_dir($ID . '/')) { if ($this->getConf('structure') == 'flat') { $posts = $this->read_dir_to_array($ID . '/', 'file', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.txt$/'); sort($posts); while (sizeof($recents) < $num && sizeof($posts)) { $post = array_pop($posts); $counter++; if ($counter > $offset) { $recents[] = $ID . '/' . $post; } } return $recents; } else { // $this->getConf('structure') == 'deep' $years = $this->read_dir_to_array($ID . '/', 'dir'); sort($years); // Now start working backwards through it all and get the most recent posts while (sizeof($recents) < $num && sizeof($years)) { $year = array_pop($years); $months = $this->read_dir_to_array($ID . '/' . $year . '/', 'dir'); sort($months); while (sizeof($recents) < $num && sizeof($months)) { $month = array_pop($months); $days = $this->read_dir_to_array($ID . '/' . $year . '/' . $month . '/', 'dir'); sort($days); while (sizeof($recents) < $num && sizeof($days)) { $day = array_pop($days); $posts = $this->read_dir_to_array($ID . '/' . $year . '/' . $month . '/' . $day . '/', 'file', '/^[0-9]{6}\.txt$/'); sort($posts); while (sizeof($recents) < $num && sizeof($posts)) { $post = array_pop($posts); $counter++; if ($counter > $offset) { $recents[] = $ID . '/' . $year . '/' . $month . '/' . $day . '/' . $post; } } } } } return $recents; } } } /** * @return String * @param Array $list * @desc Compiles the contents of all the files listed (as fully-qualified paths) * in $list into a single string. Compiles in the order listed. Adds date headers * where required and a footer after each post. */ function compilePosts($list) { global $ID, $conf; if (sizeof($list)) { $last_date = false; $str = ''; foreach ($list as $file) { // Decide if we need to add a date divider $file = str_replace('\\', '/', $file); $ts = $this->getTimestampFromFile($file); $date = date('Y-m-d', $ts); if ($date != $last_date) { $str .= $this->getDateHeader($ts); $last_date = $date; } // Add this file's contents to the output $str .= file_get_contents($file); // And add a wiki-formatted footer of meta data as well, accounting for rewrites $post_url = $this->getUrlPartFromTimestamp($ID, $ts); $edit_url = $this->getRewriteUrl($post_url, 'do=edit', false); $timestamp = date($this->getConf('datefooter'), $ts); $str .= str_replace(array('{timestamp}', '{permalink}', '{edit}'), array($timestamp, $post_url, "this>$edit_url"), $this->getConf('footer')); } return $str; } else { return ''; } } /** * @return String * @param timestamp $ts * @desc Returns a wiki-formatted date header for between posts. */ function getDateHeader($ts) { global $conf; $date = date($this->getConf('dateheader'), $ts); return $date . "\n"; } /** * @return timestamp * @param String $filename * @desc Returns a timestamp based on the filename/namespace structure */ function getTimestampFromFile($file) { global $conf; if ($this->getConf('structure') == 'flat') { $parts = explode('-', basename($file)); $ts = mktime(substr($parts[3], 0, 2), substr($parts[3], 2, 2), substr($parts[3], 4, 2), $parts[1], $parts[2], $parts[0]); } else { // $this->getConf('structure') == 'deep' $parts = explode('/', dirname($file)); $s = sizeof($parts); $date = $parts[$s-3] . '-' . $parts[$s-2] . '-' . $parts[$s-1]; $filename = basename($file); $ts = mktime(substr($filename, 0, 2), substr($filename, 2, 2), substr($filename, 4, 2), $parts[$s-2], $parts[$s-1], $parts[$s-3]); } return $ts; } /** * @return String * @param String $ID * @param timestamp $ts * @desc Returns a post url for a post based on the post's timestamp and the base ID */ function getUrlPartFromTimestamp($ID, $ts=0) { global $conf; if ($ts == 0) { $ts = time(); } /* if ($conf['userewrite'] > 0) { $sep = ($conf['useslash'] == true ? '/' : ':'); } else { */ $sep = ':'; // } if ($this->getConf('structure') == 'flat') { return $ID . $sep . date('Y-m-d-His', $ts); } else { // $this->getConf('structure') == 'deep' return $ID . $sep . date('Y' . $sep . 'm' . $sep . 'd' . $sep . 'His', $ts); } } /** * @return String * @param String $url * @param String $query * @desc Returns a url properly prefixed according to the $conf['rewrite'] option * A $page is an appropriately constructed (namespace inclusive and considering $conf['useslash']) page reference * A $query contains a query string parameters to append */ function getRewriteUrl($page, $query, $base = true) { global $conf; if ($conf['userewrite'] == 0) { if ($base) { $str = DOKU_BASE; } $str .= DOKU_SCRIPT . '?id=' . $page; if ($query != '') { $str .= '&' . $query; } } else if ($conf['userewrite'] == 1) { if ($base) { $str = DOKU_BASE; } $str .= idfilter($page, false); if ($query != '') { $str .= '?' . $query; } } else { if ($base) { $str = DOKU_BASE; } $str .= DOKU_SCRIPT . '/' . idfilter($page, false); if ($query != '') { $str .= '?' . $query; } } return $str; } /** * @return String * @param String $label * @desc Creates the HTML required for the "New Post" link, using the lable provided. */ function newPostLink($label) { global $conf, $ID; // $sep = ($conf['useslash'] == true ? '/' : ':'); $sep = ':'; //+ (isset($this->getConf('offset')) ? ($this->getConf('offset') * 3600) : 0) $page = $this->getUrlPartFromTimestamp($ID, time()); $html = '<div id="blognew">'; $hilf = $this->getRewriteUrl($page, 'do=edit'); $output = '<a href="'.$hilf.'">' . $label . '</a>'; $html .= $output.'</div>'; return $html; } /** * @return String * @param Int $page * @param String $label * @desc Creates the HTML required for a link to an older/newer page of posts. */ function pagingLink($page, $label) { global $conf, $ID; $html = '<a href="'; $html .= $this->getRewriteUrl($ID, "page=$page"); $html .= '">' . $label . '</a>'; return $html; } /** * @return Array * @param String $dir * @param String $select * @param String $match * @desc Reads all entries in a directory into an array, sorted alpha, dirs then files. * $select is used to selects either only dir(ectories), file(s) or both * If $match is supplied, it should be a / delimited regex to match the filename against */ function read_dir_to_array($dir, $select = 'both', $match = false) { $files = array(); $dirs = array(); // Read all the entries in the directory specified $handle = @opendir($dir); if (!$handle) { return false; } while ($file = @readdir($handle)) { // Ignore self and parent references if ($file != '.' && $file != '..') { if (($select == 'both' || $select == 'dir') && is_dir($dir . $file)) { $dirs[] = $file; } else if (($select == 'both' || $select == 'file') && !is_dir($dir . $file)) { if (is_string($match)) { if (!preg_match($match, $file)) { continue; } } $files[] = $file; } } } @closedir($handle); // Sort anything found alphabetically and combine the results (dirs->files) if (sizeof($dirs) > 0) { sort($dirs, SORT_STRING); } if (sizeof($files) > 0) { sort($files, SORT_STRING); } // Put the directories and files back together and return them return array_merge($dirs, $files); } /** * Create output */ function render($mode, &$renderer, $data) { global $ID, $conf; // Set the page number (determines which posts we display) if (isset($_REQUEST['page'])) { $page = $_REQUEST['page']; } else { $page = 0; } if ($mode == 'xhtml') { // Addlink for creating a new post $renderer->doc .= $this->newPostLink($this->getConf('newlabel')); // Go and get the required blog posts and compile them into one wikitext string // FIXME $config var for how many? or inline directive? $recents = $this->getPosts($ID, $this->getConf('numposts'), ($page * $this->getConf('numposts'))); $compiled = $this->compilePosts($recents); // Disable section editing to avoid weird links $conf['maxseclevel'] = 0; // Disbale caching because we need to get new files always $renderer->info['cache'] = false; // Add the compiled blog posts after rendering them. $renderer->doc .= p_render('xhtml', p_get_instructions($compiled), $info); // Add a link to older entries if we filled the number per page (assuming there's more) if (sizeof($recents) == $this->getConf('numposts')) { $renderer->doc .= '<div id="blogolder">' . $this->pagingLink($page+1, $this->getConf('olderlabel')) . '</div>'; } // And also a link to newer posts if we're not on page 0 if ($page != 0) { $renderer->doc .= '<div id="blognewer">' . $this->pagingLink($page-1, $this->getConf('newerlabel')) . '</div>'; } return true; } return false; } } ?>
Conf Changes
You'll need to add the following configuration directives as well, preferably to conf/local.php
;
Please note the new structure and offset options! |
---|
- local.php
<?php // * 2011-10-31 modified by Taggic to get is work with current dokuwiki (Rincewind) $conf['plugin']['bliki']['structure'] = 'flat'; // Structure to use when storing posts; [flat | deep] $conf['plugin']['bliki']['offset'] = 0; // Number of hours to offset new post times from your server (eg -3 if you post from a timezone 3 hours behind your server) $conf['plugin']['bliki']['numposts'] = 15; // Number of posts to show on each page of a blog $conf['plugin']['bliki']['dateheader'] = '===== l, F j ====='; // Format for date header/separator (passed to date(), escape special flags!) $conf['plugin']['bliki']['datefooter'] = 'n/j/Y g:ia'; // Format for the footer datestamp (includes hours/minutes) (passed to date(), escape special flags!) $conf['plugin']['bliki']['footer'] = ' \\\\ <sub>Posted @ {timestamp} -- [[{permalink}|Permalink]] -- [[{edit}|Edit]]</sub> \\\\ \\\\ '; // use {timestamp}, {permalink} and {edit} to link to special variables. $conf['plugin']['bliki']['newlabel'] = '» New Post'; // Label for the new post button/link $conf['plugin']['bliki']['olderlabel'] = 'Older Posts »'; // Link to older posts $conf['plugin']['bliki']['newerlabel'] = '« Newer Posts'; // Link to newer posts
The values used here are a good starting point, but you can customize them from there.
CSS Required
Bliki adds some additional elements to your page when you use it, so you have the ability to style those elements to match your template. Use the following CSS definitions in your template's CSS to alter the appearance of the elements;
- style.css
#blognew { font-size: 120%; padding: 0.5em; display: block; width: 6.5em; text-align: center; border: 1px solid #8CACBB; background: #DEE7EC; } #blognew A { color: #000000; font-weight: bold; } #blogolder, #blognewer { border: solid 1px #8cacbb; padding: 0.5em; } #blogolder { float: right; } #blognewer { float: left; }
Again, you can customize these as you see fit, but this is a good starting point.
Usage
To create a blog using bliki, you just create a new page, set up the basic template (surrounding the actual blog) and put in the ~~BLIKI~~
tag. When you load that page, you'll see any available blog posts and the option to create a new one. Your actual page might look like this;
====== My New Bliki Blog ====== Here's my new blog. ~~BLIKI~~ ---- You can even have a footer underneath your posts.
Using the deep
structure option, bliki will automatically store its entries in a namespace based on the name of the page the blog appears on, so if you create a page called newblog
and bliki'ize it, then make a post on the 7th of September, 2005 in that blog, it will create the following structure (assuming you create the post at exactly 10:00am);
newblog.txt newblog/ 2005/ 09/ 07/ 100000.txt
If you opt for the flat
structure, then your posts will be stored in a namespace named according to the name of the page you added the ~~BLIKI~~
tag to, and then named like below;
YYYY-MM-DD-HHMMSS.txt
Using the flat structure allows for simpler use of ACLs, particularly when integrating with other plugins like discussion since all the discussion happens in one namespace it is easy to open it up while locking down the posts themselves. It also allows one to use the rudimentary templating support via:
_template.txt
to setup default initial content for your new post. Huge thanks to Anthony Caetano for adding support for flat storage!
Notes
- Currently will not function properly if
$conf['userewrite']
is set to 2 and$conf['useslash']
is set to 0 (zero). - Creating a new post and editing an existing one is slightly less intuitive than it could be because DokuWiki takes you to that individual entry when you're done, rather than the blog page
- Can be combined with other plugins to provide more power (comments, better footer formatting etc)
To Do
- Take ACLs into account for new post links
- Use caching?
Thanks
- Andreas Gohr and everyone else who is actively developing DokuWiki – it's the best wiki engine out there!
- Esther Brunner for the Include plugin, which got me started on this one.
- Rael Dornfest for the blosxom blogging engine, which was the inspiration for this project.
- Anthony Caetano for adding flat storage support to the plugin
Discussion
Comments?
Thanks for the great plugin. Is there a way that the entries could be easily exported? I'd love to have it so that my entries could be exported to a calendar in iCal format.
I hadn't thought about doing an iCal version, but I was planning on including a script which would allow the addition of RSS (and maybe Atom) post feeds for bliki. I should be able to add one for iCal as well, I'll have a look at it and see how hard it would be. – Beau Lebens
Great Plugin and another than Esther's, which will work together with this one in a tremendous way. Can it be developed such as the blog-txt-files aren't stored in those numerous folders/namespaces but as a file named e.g. YYYY-mm-dd-tttttt.txt? – freddyw[at]gmx[dot]net
maybe weblog_bookmarklet might help
With the help of another developer, I'll be updating this page soon with a new version of bliki, which supports 'flatter' storage of posts (using extended names as above, rather than deep namespaces). It will hopefully also have some rudimentary timezone support. – Beau Lebens
Comments on Version 2
Thank you very much for this work. I am so keen on using them to my DW. Unfortunately, I have this configuration: Currently will not function properly if $conf[’userewrite’] is set to 2 and $conf[’useslash’] is set to 0 (zero).
, at least 'til the end of this year. Then I will pimp my hosting package to .htaccess… *G* Anyway… Can you give us an idea of what the problem is with that very config-mode? Maybe we can help?
— Freddy 2005-10-17 16:11
Freddy, if possible, please feel free to test it out - the portion of code which Anthony changed included that part, so it may actually work now - I've honestly not tested that combination rigorously under the current version. I'd love to be able to remove that caveat and have it work under any config combination. Please comment here if you find it works. – Beau Lebens 2005-10-18 12:53 PST
Just so you know, I have this installed with the “$conf[’userewrite’] is set to 2 and $conf[’useslash’] is set to 0 (zero)” settings and it's working OK. The only thing I think that it does need is the 'Edit' and 'New Post' buttons to be shown only to someone that is logged in and has the proper ACL's, but I do know that on your list of things to do. thanks for the great plugin! – Jeremy 2005-10-23
to me it seems as if “flat” isn't working, it always does “deep”. but only quick check. — Freddy 2005-10-24 15:04
A couple of ideas:
- use GMT when storing the posts
- “simply” parametrize a few items the bliki, eg ~~BLIKI:5~~, to specify 5 posts per page
- parametrize the bliki with profiles, eg ~~BLIKI:mine~~ & ~~BLIKI:mywifes~~ and have the config lookups look in the correct profile, eg $conf['bliki']['mine']['numposts'] = 5; $conf['bliki']['mywifes']['numposts'] = 15, and use the $conf['bliki']['numposts'] value as the default should a profile specific one not be available.
I just tried this plugin, and it seems to be exactly what i was looking for . One question, how do you make it work with namespace_template feature?? as far as i could see bliki does not seem to work on DokuWiki-2005-09-22 while namespace_template was not present in 2005-07-13? also one idea but not sure it is easily feasible, could author name be added in the footer? something like: “Posted by {author} @ {timestamp} – Permalink – Edit”. the author is displayed in “recent changes” so i guess there must exist some mean to access it. – Thierry 2005-11-24
This {author} entry is very interesting for me, too. Have anyone a solution for this problem?
It works for me in the 20050926 snapshot, I have a '_template.txt' file in the bliki directory which I created by hand. When I click new post the template's contents are there by default in the newly created page. Have you created the file by hand with the correct name and are its permissions such that it is readable by the webserver?
– ant 2005-11-26
ok i started from a clean install and it worked with _template. very nice . i even managed to use it with an external tool that feeds the blog from filesystem and they are displayed with browser. one last question: did you think of only displaying the first headline or first 5 lines of each post? – Thierry 2005-11-26
From — Jean-Marc Lagacé 2005-12-19 16:44 It would be nice it the plugin would respect the wiki ACL (i.e. not display the EDIT or NEW POST when the user cannot create or edit pages in the namespace).
With just a little code change, this plugin can use a namespace. All comments are stored in a tree that is modeled on the data tree. The big advantage to using a namespace is that all comments are centralized under one tree, rather than spread all over the data. Another big advantage is that by using a namespace, comments can be set to Read-Write for anyone or for a group, while the page itself is maintained as read-only. Two changes are needed:
1. Add this line to conf/local.php:
$conf['bliki']['namespace'] = "some_namespace" // ie "comments", "comments:2006", "bliki" or whatever you want
2. Replace function render()
with this:
function render($mode, &$renderer, $data) { global $ID, $conf; // Set the page number (determines which posts we display) if (isset($_REQUEST['page'])) { $page = $_REQUEST['page']; } else { $page = 0; } if ($mode == 'xhtml') { //mod 2006-02-09 save original $ID, modify it to add namespace, then change it back at the end of the function $ID_original = $ID; $ID = $conf['bliki']['namespace'] .':' . $ID; //mod 2006-02-09 ALSO CHANGE $ID BACK AT THE END!! // Addlink for creating a new post $renderer->doc .= $this->newPostLink($conf['bliki']['newlabel']); // Go and get the required blog posts and compile them into one wikitext string // FIXME $config var for how many? or inline directive? $recents = $this->getPosts($ID, $conf['bliki']['numposts'], ($page * $conf['bliki']['numposts'])); $compiled = $this->compilePosts($recents); // Disable section editing to avoid weird links $conf['maxseclevel'] = 0; // Disable caching because we need to get new files always $renderer->info['cache'] = false; // Add the compiled blog posts after rendering them. $renderer->doc .= p_render('xhtml', p_get_instructions($compiled), $info); // Add a link to older entries if we filled the number per page (assuming there's more) if (sizeof($recents) == $conf['bliki']['numposts']) { $renderer->doc .= '<div id="blogolder">' . $this->pagingLink($page+1, $conf['bliki']['olderlabel']) . '</div>'; } // And also a link to newer posts if we're not on page 0 if ($page != 0) { $renderer->doc .= '<div id="blognewer">' . $this->pagingLink($page-1, $conf['bliki']['newerlabel']) . '</div>'; } //mod 2006-02-09 -- IMPORTANT! ADD THIS LINE to change $ID back to original value $ID = $ID_original; //mod 2006-02-09 ends return true; } return false; }
[2006-02-09 dae dae [at] douglasedmunds [dot] com]
Has anyone ported Bliki to the new version of DokuWiki (2006-03-06) ?
Many questions/ideas
I tried blog:devel and bliki. None of them has all i want. I will explain what i expect from a blog:
- unique objects: success
- date/time in object name: success
- possibility to add entries in a simple way with shell scripts: success
- title: missing - can i expect a title feature like in blog:devel which is appended to the date?
- prefilled text (title of object as h1): missing
- flexibility for object name and namespace: missing - how about $conf['bliki']['object']=“%d.%m._$title”
In my situation it would be nice to have all objects grouped into one namespace e.g. season_2005-2006:28.10._new_wiki_installed. Example:
$month = strftime("%m"); $year = strftime("%Y"); if (intval($month < 9)) { $season=(strval(intval($year)-1))."-$year"; } else { $season="$year-".(strval(intval($year)+1)); } namespace="season_$season"; object=strftime("$namespace:%d.$month._") . cleanID($title);
It would be the best if i must not change anything and everybody can post blogs and they get sorted in at the correct season.
And here is the challenge: The plugin would search all posts in every season. As a solution i would recommend a parameter: archive
~~BLIKI~~ should only search in the season which is defined in line 4 of the code
~~BLIKI:archive:season_2001-2002~~ should read all objects from this specific season - new blogs are not allowed
optional: ~~BLIKI:archive~~ should read all objects but the season defined in line 4 of the code - new blogs are not allowed
I think this is a special purpose but in a club this is the reality!
If you add another parameter (month) it would be very flexible…
BTW: Do you work on bliki? Your last post is far away…
Last question: Sporadically i get the error: Warning: Cannot modify header information - headers already sent by (output started at /var/www/dokuwiki2/lib/plugins/bliki/syntax.php:421) in /var/www/dokuwiki2/inc/actions.php on line 291
— Michael Arlt 2006-10-28 22:36
some questions
- how will I be able to use categories?
- is there a simple way to modify the appearance of the entries? I'd like to have a title, instead of the Date over the entry and another color in the background of an entry and it's header…
- What ever happend to including a link in these pages to a site where the plugin is in use, as an example? — tony baldwin 2014-08-17 20:59