Rory's Blog
Home
Blog
Projects
Facebook
Google+
Github
RSS

Generating Index

04/12/15

So, I have been copying each post I write, for both my diary and my blog, into the index.html files contained in their respective areas. This was relatively painless, as I have a vim maccro that does this for me.

However, quite often I would find little errors after posting, and that meant fixing it in two places.

No more! Now I generate index.html for both my blog and my diary, and what gets published to it is based off a diary.txt file or a blogpost.txt file. To publish a new item, I just append the new file's name to the end of the list, and put that file into the correct area.

I've discussed the process while I implemented it in my diary area, and instead of retreading that in a more polished form, I'm going to discuss the current script, what I like, what I don't, and where I plan on going from here.

/*jslint node: true */
(function () {
    'use strict';
    var blogpostRead,
        readBlogFile,
        indexGenerate,
        $ = require('cheerio'),
        path = require('path'),
        fs = require('fs');

    indexGenerate = function (template, blogDIV, indexPath) {
        fs.readFile(template, 'utf-8', function (err, data) {
            var indexHTML;

            if (err) {
                return console.trace(err);
            }

            indexHTML = $(data);
            $('#content', indexHTML).html(blogDIV);

            fs.writeFile(indexPath, indexHTML, 'utf-8', function (err) {
                if (err) {
                    console.trace(err);
                }
            });
        });
    };

    readBlogFile = function (fullpath, callback) {
        fs.readFile(fullpath, 'utf-8', function (err, data) {
            var blogHTML,
                title,
                idRemove;

            if (err) {
                return console.trace(err);
            }

            blogHTML = $(data);

            title = blogHTML.find('h1').text().trim();

            blogHTML.find('h1').html(
                '\n          <a href="' + path.basename(fullpath) + '">\n'
                    + '            ' + title + '</a>\n'
                    + '        '
            );

            idRemove = function () {
                if ($(this).attr('id')) {
                    $(this).removeAttr('id');
                }
                $(this).children().each(idRemove);
            };

            blogHTML.find('.blogpost').each(idRemove);

            callback(blogHTML.find('#content,#temp').html());
        });
    };

    blogpostRead = function (area) {
        var database;

        database = path.join('database', area + '.txt');

        fs.readFile(database, 'utf-8', function (err, data) {
            var filenames,
                blogs,
                count;

            if (err) {
                return console.trace(err);
            }

            filenames = data.split('\n').filter(function (filename) {
                return filename !== '';
            });
            blogs = [];
            count = 0;
            filenames.forEach(function (filename, index) {
                var fullpath,
                    template,
                    indexPath;

                fullpath = path.join('public', area, filename);
                readBlogFile(fullpath, function (blogHTML) {
                    blogs[filenames.length - index - 1] = blogHTML;
                    count += 1;

                    if (count === filenames.length) {
                        template = path.join('templates', area + '.html');
                        indexPath = path.join('public', area, 'index.html');
                        indexGenerate(template, blogs.join(''), indexPath);
                    }
                });
            });
        });
    };

    blogpostRead('blog');
    blogpostRead('diary');
}());

Okay, so the main, anonymous function defines some functions and calls blogpostRead for "blog" and "diary".

blogpostRead = function (area) {
    var database;

    database = path.join('database', area + '.txt');

    fs.readFile(database, 'utf-8', function (err, data) {
        var filenames,
            blogs,
            count;

        if (err) {
            return console.trace(err);
        }

        filenames = data.split('\n').filter(function (filename) {
            return filename !== '';
        });
        blogs = [];
        count = 0;
        filenames.forEach(function (filename, index) {
            var fullpath,
                template,
                indexPath;

            fullpath = path.join('public', area, filename);
            readBlogFile(fullpath, function (blogHTML) {
                blogs[filenames.length - index - 1] = blogHTML;
                count += 1;

                if (count === filenames.length) {
                    template = path.join('templates', area + '.html');
                    indexPath = path.join('public', area, 'index.html');
                    indexGenerate(template, blogs.join(''), indexPath);
                }
            });
        });
    });
};

Blogpost read should maybe be called "postsRead", as it could be either a blog post, or a diary post, and it's grabbing all published posts.

This function reads the database file of interest, makes an array of filenames from it (the database file is a list of filenames in published chronological order). From there the array forEach function is used to iterate through the array aynchronously knowing the filename and index value. It is here where readBlogFile is called with the fullpath, and a callback function fed in.

The callback function is fed the file's blog html (the div with the "blogpost" class), and that html is placed at the opposite index location of the blogs array as its filename index.

....

That's a mistake. It's much better to construct the array in the same order as the filenames array, and reverse it later and when the reverse order is desired.

Anyways, this callback counts how many times it has been called, and if it has been called for every file in the filenames array, the blogs array is complete and the indexGenerator function is called.

It actually might be better to feed the indexGenerator function as input to this function, but I'm still mulling that thought over.

Overall, I feel what the function is doing is pretty clear, and I feel I executed the callback setup into readBlogFile—which should be renamed readPostFile— pretty well. It reads well, and I think I'm getting the hang of knowing when to use anonymous functions.

readPostFile:

readPostFile = function (fullpath, callback) {
    fs.readFile(fullpath, 'utf-8', function (err, data) {
        var postHTML,
            title,
            idRemove;

        if (err) {
            return console.trace(err);
        }

        postHTML = $(data);

        title = postHTML.find('h1').text().trim();

        postHTML.find('h1').html(
            '\n          <a href="' + path.basename(fullpath) + '">\n'
                + '            ' + title + '</a>\n'
                + '        '
        );

        idRemove = function () {
            if ($(this).attr('id')) {
                $(this).removeAttr('id');
            }
            $(this).children().each(idRemove);
        };

        postHTML.find('.blogpost').each(idRemove);

        callback(postHTML.find('#content,#temp').html());
    });
};

Here is the code that grabs the post div html from the file. The post has a class "blogpost", which is always a div contained inside the div with an id attribute of "content". Grabbing the content html ended up being an easier, but less readable, means of getting all the html I wanted to transfer to the index file I was generating.

Further, I want each title (always the h1 tagged data) to be a link to the post file being copied, and so I reset the h1 html to be href'd to the post of interest. The whitespace is just so that it's nicely formatted, because I still think I may end up looking at the index.html files regularly.

The rest of the stuff this function does is to handle the special case that is Impress.js: The Beginning Draft. I do a lot of special things to make that a slide-show, and those special things need to be accounted for. 1) The blogpost is contained within a "temp" div, not a "content" one. 2) Each bit of text within the blog post is encased in an id'd div. IDs are troubling because they're supposed to be unique in each file, and the id's in that file were not very likely to stay unique across a large index.html. This code removes all id attributes within every "blogpost" div, to be extra careful.

I think this action is smart overall, as again, id's that I pick aren't necessarily likely to be unique. I can come up with a smarter solution later if necessary. This function ends by calling the fed-in callback with the expected post html.

This all eventually leads to indexGenerate being called:

indexGenerate = function (template, blogs, indexPath) {
    fs.readFile(template, 'utf-8', function (err, data) {
        var indexHTML;

        if (err) {
            return console.trace(err);
        }

        indexHTML = $(data);
        $('#content', indexHTML).html(blogs.reverse().join(''));

        fs.writeFile(indexPath, indexHTML, 'utf-8', function (err) {
            if (err) {
                console.trace(err);
            }
        });
    });
};

This function takes a template html filename and a blogs array (should be renamed posts) to generate the appropriate index html to save to indexPath. The posts are reversed and joined by an empty string, and inserted into the "content" div of the template.

The template itself is just any given post with the content div empty. The only different between the diary and the blog template is that one says "Rory's Blog" and the other says "Rory's Diary".

Overall, I really like how this code turned out. It's fairly well-factored, and feels like fluent javascript. Though, I'm still far from a programmer who thinks in javascript.

Next, I want to have a publish script that will publish a file I feed to it based off the area I feed to it. Should be an easy addition. Then I'm going to need to work on making this all happen dynamically as a server feeding content to a browser, which shoudl probably start with making it so my site actually runs on node (it still runs in static nginx). I mean, locally it runs on node, but I'll need the changes I push from my chromebook to the site everybody sees to actually work on the site I push to. Meaning I'll need to do some proper setup stuff on my server.

It's been a while since I actually ssh'd there and did some stuff.

I really like how this post turned out. It's kinda a rubber-duck code reading and review. One really notable thing is that it's helping me reconsider my choice of variable names.

And if you're wondering what the script looks after this post's review:

/*jslint node: true */
(function () {
    'use strict';
    var postsRead,
        readPostFile,
        indexGenerate,
        $ = require('cheerio'),
        path = require('path'),
        fs = require('fs');

    indexGenerate = function (template, posts, indexPath) {
        fs.readFile(template, 'utf-8', function (err, data) {
            var indexHTML;

            if (err) {
                return console.trace(err);
            }

            indexHTML = $(data);
            $('#content', indexHTML).html(posts.reverse().join(''));

            fs.writeFile(indexPath, indexHTML, 'utf-8', function (err) {
                if (err) {
                    console.trace(err);
                }
            });
        });
    };

    readPostFile = function (fullpath, callback) {
        fs.readFile(fullpath, 'utf-8', function (err, data) {
            var postHTML,
                title,
                idRemove;

            if (err) {
                return console.trace(err);
            }

            postHTML = $(data);

            title = postHTML.find('h1').text().trim();

            postHTML.find('h1').html(
                '\n          <a href="' + path.basename(fullpath) + '">\n'
                    + '            ' + title + '</a>\n'
                    + '        '
            );

            idRemove = function () {
                if ($(this).attr('id')) {
                    $(this).removeAttr('id');
                }
                $(this).children().each(idRemove);
            };

            postHTML.find('.blogpost').each(idRemove);

            callback(postHTML.find('#content,#temp').html());
        });
    };

    postsRead = function (area) {
        var database;

        database = path.join('database', area + '.txt');

        fs.readFile(database, 'utf-8', function (err, data) {
            var filenames,
                posts,
                count;

            if (err) {
                return console.trace(err);
            }

            filenames = data.split('\n').filter(function (filename) {
                return filename !== '';
            });
            posts = [];
            count = 0;
            filenames.forEach(function (filename, index) {
                var fullpath,
                    template,
                    indexPath;

                fullpath = path.join('public', area, filename);
                readPostFile(fullpath, function (postHTML) {
                    posts[index] = postHTML;
                    count += 1;

                    if (count === filenames.length) {
                        template = path.join('templates', area + '.html');
                        indexPath = path.join('public', area, 'index.html');
                        indexGenerate(template, posts, indexPath);
                    }
                });
            });
        });
    };

    postsRead('blog');
    postsRead('diary');
}());

I like the script, I like this post, I have a direction to go from here, and I like the general direction and speed of my iterations on this site.

Let's hope I can keep this momentum up.