Diary SiteMap RecentChanges About Contact Calendar


Matching Pages:


RSS Feed Oddmuse is the wiki engine running all the wikis at emacswiki.org – including EmacsWiki itself. My main interests are:

See OddmuseRoadmap for my thoughts about the design of Oddmuse.

2013-12-17 PDF Button

I’m experimenting with a PDF button for this website. In the past, I suggested Print Friendly & PDF. Yesterday I learned about wkhtmltopdf, which does the same thing without depending on a remote service and their ad revenue. On a typical Debian host, you need to apt-get install wkhtmltopdf. This installs a binary and all the required libraries. The problem is that this version needs an X11 server in order to work, which you don’t have when using it on your website. In addition to a regular installation, you need to install a statically compiled binary which has been compiled with a patched version of Qt and no longer requires an X11 server.

In your Oddmuse config file:

$Action{pdf} = \&DoPdf;
push(@KnownLocks, 'pdf');

sub DoPdf {
  my $id = shift;
  local $StyleSheet = 'http://alexschroeder.ch/alex-2012.css';
  my $html = PageHtml($id);
  my $source = "$TempDir/document.html";
  my $status = '500 INTERNAL SERVER ERROR';
  open(HTML, '>:utf8', $source)
    or ReportError("Cannot write $source: $!", $status);
  # see GetHeader
  print HTML GetHtmlHeader(NormalToFree($id), $id);
  print HTML $q->start_div({-class=>'header'});
  print HTML $q->h1({-style=>'font-size: x-large'}, GetPageLink($id));
  print HTML $q->end_div(); # header
  print HTML $q->start_div({-class=>'wrapper'});
  # get rid of letter-spacing
  my $sperrung = '<em style="font-style: normal; letter-spacing: 0.125em; padding-left: 0.125em;">';
  $html =~ s/$sperrung/<em>/g;
  my $newthought = '<em style="font-style: normal; font-variant:small-caps; letter-spacing: 0.125em;">';
  $html =~ s/$newthought/<em style="font-style: normal; font-variant:small-caps">/g;
  print HTML $html;
  # see PrintFooter
  print HTML $q->end_div(); # wrapper
  print HTML $q->start_div({-style=>'font-size: smaller; '});
  print HTML $q->hr();
  print HTML $FooterNote;
  # see DoContrib
  SetParam('rcidonly', $id);
  SetParam('all', 1);
  my %contrib = ();
  for my $line (GetRcLines(1)) {
    my ($ts, $pagename, $minor, $summary, $host, $username) = @$line;
    $contrib{$username}++ if $username;
  print HTML $q->p(Ts('Authors: %s',
                      join(', ', map { GetPageLink($_) }
                           sort(keys %contrib))));
  print HTML $q->end_div(); # footer
  print HTML $q->end_html;
  print HTML "\n";
  my $target = "$TempDir/document.pdf";
  my $error = `/home/alex/bin/wkhtmltopdf --print-media-type --quiet '$source' '$target'`;
  ReportError("The conversion of HTML to PDF failed", $status) if $error;
  open(PDF, '<:raw', $target) or ReportError("Cannot read $target: $!", $status);
  local $/ = undef;
  my $pdf = <PDF>;
  ReportError("$target is empty", $status) unless $pdf;
  binmode(STDOUT, ':raw');
  print GetHttpHeader('application/pdf');
  print $pdf;

sub PrintMyContent {
  my $id = UrlEncode(shift);
  if ($id and $IndexHash{$id}) {
print qq{
<form action="$FullUrl"><p>
<input type="hidden" name="action" value="pdf" />
<input type="hidden" name="id" value="$id" />
<input type="submit" value="PDF" />

Let me know if it works for you while I try to figure out whether I need this at all. The position of the PDF button at the very bottom of the page is probably less than ideal.

As you can tell, the markup using increased Wikipedia:letter-spacing is messing it all up, which is why I had to fix it.

Tags: RSS

Comments on 2013-12-17 PDF Button

Hi Alex, Long time ago, I wondered about to include a patch like this for my wiki, but eventually most browsers have an utility like this, and others like breadcrumbs, etc.. Is there any advantage?. Thanks.

JuanmaMP 2013-12-19 08:52 UTC

On my Mac, I don’t need it. Printing to PDF is simple. On Windows, however, I need to install a software PDF printer if I want to do this. Just recently my sister asked me for help converting a Word document she had written. She didn’t manage the installation of the PDF printer software. That reminded me of the fact that for some users, a PDF button might still be necessary. I personally don’t like the “save as HTML” feature of most browsers because it results in HTML + a directory of CSS files, images, ads, scripts, and so on. PDF feels “safe”.

AlexSchroeder 2013-12-19 09:03 UTC

Add Comment

2013-12-01 Anonymizing the Oddmuse log files

I’ve just implemented a new non-optional Oddmuse feature. I’m removing all hostnames and IP numbers of older log entries. The log entries older than 90 days are stored in a different log file in order to speed up the generation of RecentChanges. During maintenance, these log entries are copied from one file to the other and I’m now taking advantage of this copying to remove the hostname or IP number.

Basically I find that as a person, I dislike invasions of privacy and I feel that in some small form, software engineers are inviting it because often it’s easier to do. We often model things to never forget, e.g. version control.

One of the important pages on Meatball was ForgiveAndForget. Forgetting is human.

At the same time, with Snowden and the NSA, I feel that as a hoster I’m more comfortable if I cannot provide the logs an agency is looking for.

Furthermore, I’ve had a very small number of emails from users asking me to remove their hostnames from the log files because they had accidentally edited the wiki from work. Pages containing their hostname will eventually be deleted but log entries were not. Now they’re anonymized and people can feel safer knowing that the traces will eventually disappear again.

The idea is that you would only need hostnames or IP numbers to fight spam and vandalism: Add regular expressions matching either hostname or IP number of spammers or vandals to your list of banned hosts and prevent the attack from continuing. After a few days, however, this information is no longer required. In this day and age of privacy invasion, I think software should take a pro-active stance. The log entries must be anonymized.

The existing log file for the older entries is not changed. If you want to do the right thing, there’s a script called anonymize.pl in the contrib directory to do just that.

Just call it in your data directory. Example:

alex@psithyrus:~/oddmuse$ perl ~/src/oddmuse/contrib/anonymize.pl 
Wrote anonymized 'oldrc.log'.
Saved a backup as 'oldrc.log~'

See Oddmuse:Upgrading Issues for a more technical explanation of what’s going on.


Add Comment

2013-08-14 Comments on this Wiki Blog

I’ve done a few changes. Let’s see whether it works out.

  1. when viewing ordinary pages, previous comments and the comment form are shown ← this is what I wanted to add
  2. people who follow the RSS feeds will therefore easily find the obvious comment form when following the link ← this is what I hope to achieve
  3. when leaving a comment, you end up on the comment page, which might be confusing
  4. when looking at RecentChanges, which has some extra magic associated with its name, the comments are not shown
  5. when looking at older revisions, the page history, and many other variants, the comments are not shown
  6. when looking at a journal page such as Diary, the comments are not shown
  7. when looking at a journal page such as Diary, you can still see links to inline the comment page
  8. inlined comment pages don’t come with an automatic comment form—you need to click on the “Add comment” link at the end
  9. comment pages are still excluded from the usual feeds (I wonder whether I should change this)

I think the Wiki + Blog combo still works. I’m just trying to make it less weird. :)

Source code for your config file, if you’re an Oddmuse user. The source code below also includes my Google +1 setup (Oddmuse:Google Plus One Module) because my code needs to avoid the situation where a page shows two +1 buttons. As for comments within journals, I use Oddmuse:Dynamic Comments Extension.

# Google +1 list

push(@MyAdminCode, sub {
       my ($id, $menuref, $restref) = @_;
       push(@$menuref, ScriptLink('action=plusone',
				  T('Google +1 Buttons'),

$Action{plusone} = \&DoPlusOne;

sub DoPlusOne {
  print GetHeader('', T('All Pages +1'), ''),
    $q->start_div({-class=>'content plusone'});
  print $q->p(T("This page lists the twenty last diary entries and their +1 buttons."));
  my @pages;
  foreach my $id (AllPagesList()) {
    push(@pages, $id) if $id =~ /^\d\d\d\d-\d\d-\d\d/;
  splice(@pages, 0, $#pages - 19); # last 20 items
  print "<ul>";
  foreach my $id (@pages) {
    my $url = ScriptUrl(UrlEncode($id));
    print $q->li(GetPageLink($id),
		qq{ <g:plusone href="$url"></g:plusone>});
  print "</ul>";
  print $q->end_div();

# two step Google +1 button to protect your privacy
# http://my.opera.com/QuHno/blog/adding-the-google-1-button-to-a-webpage-without-violating-the-users-privacy


sub MyNewGetCommentForm {
  return MyOldGetCommentForm(@_) . q{
<script type="text/javascript">
function loadScript(jssource,thelink) {
   var jsnode = document.createElement('script');
   document.getElementById(thelink).innerHTML = "";
 var plus1source = "https://apis.google.com/js/plusone.js";
<p id="plus1">
  <a href="javascript:loadScript(plus1source,'plus1')">
    <img src="/pics/plusone-h24.png" alt="Show Google +1" />
<!-- <g:plusone></g:plusone> -->
<div class="g-plusone" id="my_plusone"></div>
<script type="text/javascript">
  document.getElementById("my_plusone").setAttribute("data-size", "medium");
  document.getElementById("my_plusone").setAttribute("data-href", document.location.href);

# make sure journal pages set a global variable which we then use to
# hide the comment form

*MyOldPrintJournal = *PrintJournal;
*PrintJournal = *MyNewPrintJournal;

my $MyPagePrintedJournal;

push(@MyInitVariables, sub {
       $MyPagePrintedJournal = 0;

sub MyNewPrintJournal {
  $MyPagePrintedJournal = 1;
  return MyOldPrintJournal(@_);

# list comments and comment form at the bottom of every normal page

*MyOldPrintFooter = *PrintFooter;
*PrintFooter = *MyNewPrintFooter;

sub MyNewPrintFooter {
  my ($id, $rev, $comment) = @_;
  if (!$MyPagePrintedJournal
      and GetParam('action', 'browse') eq 'browse'
      and $id and $CommentsPrefix
      and $id ne $RCName
      and $id !~ /^$CommentsPrefix(.*)/o) {
    my $target = $CommentsPrefix . $id;
    my $page = '';
    $page = PageHtml($target) if $IndexHash{$target};
    print $q->div({-class=>'comment'},
    # don't include Google +1 button twice
    print MyOldGetCommentForm("$CommentsPrefix$id", $rev, $comment);


Add Comment

2013-01-14 Javascript

I just rewrote my Tag Cloud to use the Google Treemap.

What it does is the following:

  1. The cloud action runs the old code (text representation).
  2. The tagcloud action runs the new code.
  3. The new code basically takes the example Google Treemap and replaces the data with my tags. These tags are not nested.
  4. On click, we call the tag action which shows the last blog pages with that tag.

Try it! → Tag Cloud. (I realize that the tag cloud isn’t very interesting in and of itself. I just enjoyed using the Google tools and learning a little bit of Javascript on the way.)

$Action{cloud} = $Action{tagcloud};
$Action{tagcloud} = \&MyTagCloud;

sub MyTagCloud {
  print GetHeader('', T('Tag Cloud'), '');
  # open the DB file
  require DB_File;
  tie %h, "DB_File", $TagFile;
  my $max = 0;
  my $min = 0;
  my %count = ();
  foreach my $tag (grep !/^_/, keys %h) {
    $count{$tag} = split(/$FS/, $h{$tag});
    $max = $count{$tag} if $count{$tag} > $max;
    $min = $count{$tag} if not $min or $count{$tag} < $min;
  untie %h;
  # ignore 90% of all tags
  my @values = sort values %count;
  $min = GetParam('min', $values[int($#values * 0.9)]);
  # https://developers.google.com/chart/interactive/docs/gallery/treemap
  print <<EOT;
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript">
      google.load("visualization", "1", {packages:["treemap"]});
      function drawChart() {
        // Create and populate the data table.
        var data = google.visualization.arrayToDataTable([
          ['Location', 'Parent', 'Mentions', ''],
          ['Tags', null, 0, 0],
  foreach my $tag (sort keys %count) {
    my $n = $count{$tag};
    next unless $n > $min;
    print "          ['$tag', 'Tags', $n, 0],\n";
  print <<EOT;
        // Create and draw the visualization.
        var tree = new google.visualization.TreeMap(document.getElementById('treemap'));
        google.visualization.events.addListener(tree, 'select', selectHandler);
        function selectHandler() {
          var selection = tree.getSelection();
          var item = selection[0];
          window.location = "/alex?action=tag;id=" + data.getValue(item.row,0);
  print $q->start_div({-class=>'content cloud'});
  print $q->p(ScriptLink('action=tagcloud;min=0', T('Include all tags')),
              Ts('(currently showing tags with more than %s occurences)', $min));
  print $q->p(Ts('Or switch to a %s.', ScriptLink('action=cloud', T('text format'))));
  print $q->start_div({-id=>'treemap', -style=>'height: 1000px;'});
  print $q->end_div();
  print $q->end_div();


Add Comment

2012-08-14 Two-step verification

My websites have not been sending any email notifications out to people who had subscribed to page edits. The reason was that I had switched my Google account to use 2-step verification. Only today did I realize that the error messages were trying to tell me that I needed an application specific password for my script.

Everything should be back in working order. :)

I feel relieved. For a while I thought I’d have to dig around in the Perl SMTP libraries. Ugh!

And I feel much better with 2-step verification. The Mat Honan story was scary.

Tags: RSS

Add Comment

2012-03-07 Tinkering with the CSS

I’ve started tinkering with the CSS again. No more secret theme switching by clicking on my face up there. :)

I’m going for black and white and the default blue for all links. The previous classification for site links, links to sister sites, links to Wikipedia and all other links didn’t really work with all the redirections I had implemented. Thus, be gone!

I’m sticking to Garamond as my favorite font even though I must admit it looks absolutely horrible when viewed on Windows without Clear Type. Oh well! Most people will be reading the site via a feed reader anyway, I guess.

I’ll also try to stick to a larger than average font-size. I haven’t decided whether I should be using Google Web Fonts.

Let me know if there is something you’d like to see changed. :)


Comments on 2012-03-07 Tinkering with the CSS

Andreas Gohr
How does the referrer thingy at the bottom of the post work? The two pages linked seem not to link to this post?

Regarding styling: I’d prefer a non-serif font.

Andreas Gohr 2012-03-08 10:14 UTC

The referrer thing works as follows: if a visitor to the wiki has referrer information, the wiki fetches this page and searches for a link to the page. If that works, it remembers the link. This means that eventually referrals expire if nobody ever follows them. Normally this works very well. Some sites such as Blogspot offer users a blog-roll. As far as I understand it, users will then see the n most recently updated entries or a random selection. Either way, by the time other visitors try and check the referral, it’s no longer being displayed.

If you check Self:action=refer you will notice that at the moment the authors of Dreams of Mythic Fantasy, The City of Iron and Troll and Flame seem to have me listed on their blog-roll. All of them are Blogspot blogs. :)

As for sans-serif, try this link. (Your preference is set in a cookie.) Feel free to switch back.

AlexSchroeder 2012-03-08 11:46 UTC

For Pierre and others who preferred the old look: Try this! switch to the Red Beige theme and switch back to the default theme

AlexSchroeder 2012-03-23 15:28 UTC

Add Comment

2012-02-02 Oddmuse Channel

This is why I like the Oddmuse channel on Freenode so much… :)

16:51 CapnDan
Honestly I don’t understand why people get so religious about different versions of Unix.
16:52 kensanata
Everybody likes to imagine that their decision was rational.
16:52 kensanata
Therefore they infuse the first decision and the facts that influenced it with overproportional importance.
16:52 kensanata
16:53 CapnDan
yeah, there’s a name for that, I was reading about it a few months ago.
16:53 CapnDan
A popular psychology word from last year.
16:53 kensanata
“Moron Syndrome”?
16:53 CapnDan

(Dan later discovers that the term he was looking for is cognitive inertia.)


Add Comment

2011-10-07 Oddmuse, Venus and Perl


I’ve been working on a submission form for the Old School RPG Planet. Today I added another little feature. This is how I like to develop code. No time pressure. One little step at a time. Keep polishing it.

The planet uses Planet Venus to collect the RSS and Atom feeds of many of the Old School RPG blogs out there. Planet Venus allows you to get the list of feeds via an URL. I’m hosting the list of feeds on Campaign Wiki itself (raw format). As you can see, it the format doesn’t look nice.

The thing I did, therefore, was to write a script that makes it easy for people who are not into the technical details to submit new blogs. It also makes it easier for me to submit new blogs!

The things it handles:

  • If you submit an invalid URL, it will prepend http:// and try again.
  • If it looks like we already have a similar looking feed on our page, it requires a confirmation by the user.
  • If you submit a web page, it will look for alternative links with MIME types application/rss+xml, application/atom+xml, application/xml (yeah) and text/xml (just making sure) and allow the user to pick one of them.
  • If you submitted a feed directly instead of a web page, it it uses that instead.
  • If the feed you picked is served with an invalid content type, it is rejected.
  • It extracts the title of the feed and adds it to the wiki page, sorting all the entries alphabetically.

I think it’s pretty cool.

If you look at the interface, you’ll note that it has a link to its own source code. I love this little Perl trick:

  1. Add __DATA__ at the end of the source file. Usually you would add actual data at the end. The script could read it using the DATA file handle.
  2. Serve source code using seek DATA, 0, 0; print "Content-type: text/plain; charset=UTF-8\r\n\r\n", <DATA>; This resets the current position of the DATA file handle to the beginning of the source file. Tadaa! :)


Add Comment

2011-07-06 Regular Expression To Validate Id Attributes

I noticed that my blog no longer validated as XHTML 1.0 and I started investigating. On the Diary page, you can click on the comment links of the various blog posts (such as Comments on 2011-07-05 Google Plus) and you’ll get the comments inlined. This uses a tiny piece of javascript (and some CSS):

function togglecomments (id) {
   var elem = document.getElementById(id);
   if (elem.className=="commentshown") {
   else {

Thus, the HTML source already includes the comments in an appropriate div:

<div class="commenthidden" id="Comments_on_2011-07-05_Google_Plus">

Links such as Comments on 2011-07-05 Google Plus will simply call the javascript function defined above and pass the id of the div to toggle:

<a href="javascript:togglecomments('Comments_on_2011-07-05_Google_Plus')">Comments on 2011-07-05 Google Plus</a>

That’s why the id attribute is important. The trivial solution is to simply use the blog post title (“2011-07-05 Google Plus”) but soon enough you’ll note that there are some interesting restrictions on the values of id attributes:

  • may start with a colon, a letter, or underscore
  • the rest of the name may contain the above and dashes, periods, and numbers
  • brackets, braces, and parenthesis are not allowed

Now—how exactly is this defined? See the definition of Name in the XML spec:

  • NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
  • NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
  • Name ::= NameStartChar (NameChar)*

Now, those are Unicode code points. But sadly, Oddmuse tries to be encoding agnostic. (I will have to revisit this decision, soon!)

Here’s a simple beginning of a regular expression that would identify well-formed names: /^[:_A-Za-z][-.:_A-Za-z0-9]*/

Now to extend it using the information above:

   Unicode Codepoint   UTF-8 encoding
   [#xC0-#xD6]              c3 80 - c3 96
   [#xD8-#xF6]              c3 98 - c3 b6
   [#xF8-#x2FF]             c3 b8 - cb bf
   [#x370-#x37D]            cd b0 - cd bd
   [#x37F-#x1FFF]           cd bf - e1 bf bf
   [#x200C-#x200D]       e2 80 8c - e2 80 8d
   [#x2070-#x218F]       e2 81 b0 - e2 86 8f
   [#x2C00-#x2FEF]       e2 b0 80 - e2 bf af
   [#x3001-#xD7FF]       e3 80 81 - ed 9f bf
   [#xF900-#xFDCF]       ef a4 80 - ef b7 8f
   [#xFDF0-#xFFFD]       ef b7 b0 - ef bf bd
   [#x10000-#xEFFFF]  f0 90 80 80 - f3 af bf bf

I started writing the following regular expression:

$regexp = "|\xc3[\x80-\x96\x98-\xb6\xb8-\xff]|[\xc4-\xca].|\xcb[\x00-\xbf]"
        . "|\xcd[\xb0-\xbd\xbf-\xff]|[\xce-\xDF].|\xe0..|\xe1[\x00-\xbe]."
        . "|\xe1\xbf[\x00-\xbf]|\xe2\x80[\x8c\x8d]"
    if $HttpCharset eq 'UTF-8';
$id = ":$id" unless $id =~ /^[:_A-Za-z]$regexp/;
return join('', $id =~ m/([-.:_A-Za-z0-9]$regexp)/g);

Then I got tired and though, “if anybody reports an error, I’ll add the rest…”


Comments on 2011-07-06 Regular Expression To Validate Id Attributes

You do know RegEx match open tags except XHTML self-contained tags?

– Harald 2011-07-12 12:00 UTC

Habe ich schon mal gesehen, ja. Und kennst du Oh Yes You Can Use Regexes to Parse HTML!?

In meinem Fall geht es aber nicht um Parsen von HTML sondern um die Transformation von Wiki Seitentiteln zu id Werten, welche ich im generierten HTML dann verwenden kann.

AlexSchroeder 2011-07-12 12:15 UTC

Add Comment

2011-01-24 Spam

Wow, what a spam storm! I rolled back all those changes and locked the wiki for the moment. There’s no fighting a bot manually. :)

I’ll look into what allowed this to happen later today and hopefully enable page editing and commenting again. ;-)

Update: IP number blocked, lock removed. That was weird. vee


Add Comment


Show Google +1

EditNearLinks: MultilingualWiki WikiLog ForgiveAndForget FeatureKarma

The same page on other sites: