Subscribe to House of curiosities        RSS Feed
***** 1 Votes

D.I.C Frequent Poster's Utility

Icon 3 Comments
One thing that bothers me about D.I.C board is its textarea box. If you're a frequent writer/poster, as I have been for a short time, about a year ago, you tend to get tired of regular bbCode. Or, well, at least I do. On various forums where I (sometimes) write I usually don't use formatting that much, because frankly, who needs it? You want your post to be clear and readable, not colorful and blinking. Or I hope you do. ;)

Programming boards however are different in that respect. For your message to be readable and clear it's often necessary to use a lot of formatting. One thing that's plenty useful is inline code. It is quite bothersome though to write
[il]a[/il]
all the time to signify that you mean variable a. That's one, two... Nine keystrokes! Or four clicks, if you're a mouse lover user (I tend to leave it alone when writing).

One solution would be to replace the existing bbCode implementation with something more akin to wiki notation, however that would most probably confuse a lot of people. Of course I have no means of changing how D.I.C server-side software operates... I do however have means of modifying client-side, so why not try it? ;)

At first I was just going to add shorter inline version to speed up typing speed, it was supposed to be a short and primitive snippet, but as it happened, I turned it into a parser, just because I had a bit of free time.

At the moment I'm using Debian Sid and Chromium as my main browser (well, Opera was a bit too slow on my, already very slow and heavily RAM-impaired, laptop). That means that I manage my "userscripts" (if any) by Tampermonkey, a Chromium version of original Greasemonkey add-on for Firefox, that I'm sure most of you here know.

I'm not really into Javascript that much, even though I like the language flexibility. I certainly won't stop you from pointing out any dubious parts in the script. This is a version that I'd call "I-think-it's-working-now", so it's not heavily tested. Let me explain in short what the script does:

The script replaces certain character sequences that it searches for in what you just wrote. At first I made it to replace things live, or "when you type" - it was useful for debugging the stuff for a bit. Later on I thought it wouldn't really be all that great to have a script running on every oninput event. Though, as I turned it into a parser now (it was based on regex before, with cutting out code and il parts as to not mess with their contents), it could also result in strange interesting behaviour... Like live tag autocompletion. Hm... Nah, I'll live it as it is at the moment.

In short, when you submit the form, the script will replace:
''foo''  ->  [il]foo[/il] (tapping '' is quite fast, at least for me ;P)
- removed other shortcuts for now
- added /tag*/ notation 
/tag:some stuff/  ->  [tag]some stuff[/tag]
/tag::ARGSNOSPACES some stuff/  ->  [tag=ARGSNOSPACES]some stuff[/url]
/tag::"args with spaces" some stuff/  ->  [tag=args with spaces]some stuff[/url]
- At the moment [quote] tag is special in that it will not add '=' after its name, to accomodate do DIC quote tag
/quote::"args with spaces" some stuff/  ->  [quote args with spaces]some stuff[/quote]
- I usually use it when posting urls:
/url::link_without_spaces Name name!/  ->  [url="link_without_spaces"]Name name![/url]
                         ^ this space is a separator between link and its name



The thing is, it won't shouldn't mess with your source code in code tags, so you can post those pointers to pointers and variable variables or SQL comments, etc. and it wouldn't break it. It treats il tags the same, however you need to remember that it's only a translation layer between shorter notation and bbCode, how it will all look on the preview depends on the server-side bbCode parser in the board software.
There's a side effect I anticipated (I have written a similar parser before, but in different language) that the parser will try to fix your close-tags. That means that if you write:
[quote]What's up, Doc?[/qiote]
the script will show you a warning about it and just in case, will not submit the post so you may review it. Still, it will fix it to look like this:
[quote]What's up, Doc?[/quote]

Actually, you can close any kind of bbCode tag with simple [/] (with the exception of code and il tags) , and it will be fixed automatically (last open tag will be closed). Also, if you forget to close a tag, it will be closed for you too, usually at the the end of the message. There's this one thing though, if you have many tags, like:
[a]aaa[b]bbb[c]ccc[d]ddd[oops]ddd[/d]ccc[/c]bbb[/b]aaa[/a]
The oops tag will be closed on first [/, so all the closing-tags are going to "step one place right":
[a]aaa[b]bbb[c]ccc[d]ddd[oops]ddd[/oops]ccc[/d]bbb[/c]aaa[/b][/a]

That may, or may not, be a problem for you. Luckily, with the warnings you also get a button that will revert changes. It's on the top-right and will stay there until you either click the x, or the button itself. Oooor, well, since there already are revert/redo buttons on the editor, I might just get rid of the button in the next revision. ;)
Note that at the moment only tags included in all_tags array are going to be auto-closed and fixed. These I took from the help page on bbCode on D.I.C.

edit: I updated the code recently, fixing it a little - but some bugs are left out, so YMMV. I also added a little side effect - the script changes fast-reply box to be fixed on the bottom of your browser window. I tend to scroll all over the place when replying to a topic, so it works for me. ;)

Now, if this actually works is also tested by this very post, which was written using my shorthand notation. ;) Enough babbling, here's the code:
// ==UserScript==
// @name       D.I.C Frequent Poster's Utility
// @namespace  URI
// @version    0.5
// @description  I aim to create something that will augment posting speed in DIC forums.
// @match      http://www.dreamincode.net/forums/*
// @copyright  2012+, Tomasz Wota <tomasz.wota@gmail.com>
// ==/UserScript==
// yeah yeah, I know. ideas how to get rid of those globals without "pretending" you don't use globals or hardcoded values?
var dic_helper = {
    editor: null,
    original_version: '',
    revert: function() {
        this.editor.value = this.original_version;
    }
};
function log(s) {
    console.log(s);
    //document.getElementById("log").value += "> " + s + "\n";
}
function logg(s) {
    console.log(s);
    //document.getElementById("log").value += s;
}
function pretty_warning(text) {
    var id = "warning-box-12312313123";
    var box = document.getElementById(id);
    if (!box) {
        box = document.createElement('div');
        var title = document.createElement('div');
        title.appendChild(document.createTextNode("D.I.C Frequent Poster's Utility\nClick this box to close it."));
        title.style.whiteSpace = 'pre-wrap';
        title.style.textAlign = 'center';
        title.style.color = 'orange';
        title.style.backgroundColor = '#787878';
        box.appendChild(title);
        box.appendChild(document.createElement('div'));
        box.lastChild.innerHTML = text; // yes, I know, I didn't want to use it but I did want my log to have html... and fast. Sorry.
        box.style.whiteSpace = 'pre-wrap';
        box.style.fontFamily = 'monospace';
        box.style.fontSize = '12px';
        box.style.position = 'fixed';
        box.style.top = "0";
        box.style.left = "0";
        box.style.width = "100%";
        box.style.paddingTop = '0';
        box.style.paddingBottom = '20px';
        box.style.color = "orange";
        box.style.backgroundColor = "#454545";
        box.style.borderTop = "3px solid orange";
        box.style.borderBottom = "3px solid orange";
        box.setAttribute("id", id);
        document.body.appendChild(box);
        
        var revert = document.createElement('div');
        revert.style.position = 'fixed';
        revert.style.top = '10px';
        revert.style.right = '10px';
        revert.style.cursor = 'pointer';
        revert.style.color = 'orange';
        revert.style.fontFamily = 'monospace';
        revert.style.backgroundColor = '#787878';
        revert.style.border = "1px solid orange";
        var button = document.createElement('input');
        button.setAttribute('type', 'button');
        button.value = 'Revert changes';
        button.onclick = function() {
            dic_helper.revert();
            revert.parentNode.removeChild(revert);
            return false;
        };
        revert.onclick = function() {
            if (revert && revert.parentNode) {
                revert.parentNode.removeChild(revert);
            }
        };
        revert.appendChild(button);
        revert.appendChild(document.createTextNode(' x'));
                           
        document.body.appendChild(revert);
        box.onclick = function() {
           box.parentNode.removeChild(box); 
        };
    } else {
        box.appendChild(document.createElement('div'));
        box.lastChild.innerHTML = text;
    }
}
Array.prototype.last = Array.prototype.last || function(count) {
    count = count || 1;
    var length = this.length;
    if (count <= length) {
        return this[length - count];
    } else {
        return null;
    }
};
// some globals, you could say it's a configuration
// they are used later on in the parser, so don't throw them away without fixing the code ;)
var short_tags = [ "''" ];
var long_tags  = [ "il" ];
var skip_parsing_tags = [ 'php', 'html', 'sql', 'code', 'inline', 'il', 'xml' ];
var single_tags = [ 'hr', 'member' ];
var all_tags = [ 'il', 'inline', 'code', 'php', 'xml', 'html', 'sql',
                 'snapback', 'right', 'left', 'center', 'topic', 'post', 'spoiler',
                 'acronym', 'b', 'i', 'u', 'hr', 'url', 'img', 'quote', 'indent',
                 'list', 'strike', 's', 'sub', 'sup', 'email', 'background', 'color',
                 'size', 'font', 'member', 'twitter', 'extract', 'blog', 'entry', 'media'
               ];
var reps = [];
short_tags.forEach(function(el, i, a){
    reps.push([ short_tags[i], long_tags[i] ]);
});
var short_tags_starting_characters = [];
short_tags.forEach(function(el, i, a){
    short_tags_starting_characters.push(el[0]);
});


var Tag = function(name, parent) {
    this.name = name;
    this.long_name = this.get_long_name();
    this.args = "";
    this.content = [];
    this.parent = parent;
    this.skip_parsing = (skip_parsing_tags.indexOf(this.long_name) != -1);
    this.is_short = (name.length && this.name === this.long_name);
    this.no_tail = (single_tags.indexOf(this.long_name) != -1);
    this.valid = (all_tags.indexOf(this.long_name) != -1);
};
    
Tag.prototype.get_long_name = function() {
    var n = this.name.toLowerCase();
    reps.forEach(function(el, i, a){
        n = n.replace(el[0], el[1]);
    });
    return n;
};

Tag.prototype.show_args = function() {
    var args = "";
    if (this.long_name === 'quote') {
        args = this.args;
    } else {
        if (this.args.length) {
            if (this.args[0] === '=') {
                args = this.args.substr(1);
            }
            args = '=' + this.args;
        } else {
            args = '';
        }
    }
    return args;
};

Tag.prototype.get_unwinded_contents = function() {
    var output = ""; 
    if (this.name.length) {
        output += "[" + this.long_name + this.show_args() + "]";
    }
    this.content.forEach(function(el, i, a){
        if (el instanceof Tag) {
            output += el.get_unwinded_contents();
        } else {
            output += el;
        }
    });
    
    if (!this.no_tail && this.valid) {
        output += this.name.length ? "[/" + this.long_name + "]" : "";
    }
    return output;
};

Tag.prototype.push = function(data) {
    if (data instanceof Tag || this.content.length === 0) {
        this.content.push(data);
    } else {
        if (this.content[this.content.length - 1] instanceof Tag) {
            this.content.push(data);
        } else {
            this.content[this.content.length - 1] += data; 
        }
    }
};


var Parser = function(text) {
    this.text = text;
    this.warnings = 0;
};
    
Parser.prototype.get_bbCode = function() {
    var input = this.text.split("").reverse();
    var current = new Tag("", null);
    var name = "";
    var prevent_push = false;
    var tag = null;
    while (input.length) {
        var args = "";
        var c = input.pop();
        prevent_push = false;
        if (c === '[') {
            if (input.last() !== '/') { // if c is a start of a bbTag:
                if (current.skip_parsing === false) {
                    name = "";
                    while ([ '=', ' ', ']' ].indexOf(input.last()) == -1) {
                        name += input.pop();
                    }
                    if (all_tags.indexOf(name) == -1) { // if not a valid tag
                        while (name.length > 0) { // push back these characters
                            c = name[name.length - 1];
                            name = name.substr(name.length - 1); 
                            input.push(c);
                        }
                        c = '['; // show [
                        
                    } else {
                        // get arguments, if any
                        args = "";
                        while (input.length && input.last() !== ']') {
                            args += input.pop();
                        }
                        if (input.length === 0) {
                            /// @todo: what if consumed everything till EOF?
                        
                        } else {
                            input.pop(); // pop dangling ']'
                            
                            tag = new Tag(name, current);
                            tag.args = args;
                            current.push(tag);
                            if (!tag.no_tail) {
                                current = tag;
                            }
                            prevent_push = true;
                        }
                    }
                } 
                
            } else { // or an end of it
                input.pop(); // pop '/'
                name = "";
                while (input.last() !== ']') { // makes else unreachable unless it's end of file, which is a bummer
                    name += input.pop();
                }
                
                
                if (input.last() === ']') {
                    input.pop(); // pop ']'
                    if (current.skip_parsing === false) {
                        if (current.name !== name) {
                            pretty_warning("You opened tag <strong>[" + current.long_name + "]</strong> but tried to close it with <strong>[/" + name + "]</strong>. I've fixed that for you, whether for good or bad.");
                            this.warnings += 1;
                        }
                        current = current.parent;
                        prevent_push = true;   
                    } else { // we're not parsing, but be careful
                        if ((current.long_name === 'code' && name === 'code') || (current.long_name === 'il' && name === 'il')) { // we still want to close our code/il tag, right? ;)
                            current = current.parent;
                            prevent_push = true;
                        } else {
                            c = "[/" + name + "]";
                        }
                    }
                } else { // not reachable at the moment, see while loop above
                    c = "[/" + name;
                }
            }
            
        } else if (short_tags_starting_characters.indexOf(c) != -1 && input.last() === c) { // so it's a short-tag, possibly, two identical chars
            if (current.skip_parsing) {
                if (current.name === "code") {
                    // we just let it pass, and wait for closing /code 
                } else if (c === "'" && current.name === "''") { // okay, time to close this tag down
                    input.pop(); // pop dangling "'"
                    current = current.parent;
                    prevent_push = true;
                } 
            } else { // we're not skipping parsing
                input.pop(); // dangling char
                
                name = "" + c + c;
                if (name === current.name) { // end tag, obviously
                    current = current.parent;
                } else { // start tag, I think
                    tag = new Tag(name, current);
                    current.push(tag);
                    current = tag;
                }
				prevent_push = true;
            }
        } else if (c === '/' && current.skip_parsing === false) { // or maybe  /tag::arguments text/   /tag:text/  ?
            name = "";
            
            while (input.last() !== ':' && input.last().match(/\w/)) { // get name till :
                name += input.pop();
            }
            tag = new Tag(name, current);
            if (tag.valid && input.last() === ':') {
                input.pop(); // pop ':'
                args = "";
                if (input.last() === ':') { // read arguments first
                    input.pop(); // pop ':'
                    if (input.last() === '"') { // if it starts with ", including spaces and everything
                        while (input.last() !== '"') { // read till matching "
                            args += input.pop();
                        }
                        input.pop(); // pop the dangling "
                    } else { // normal arguments, no spaces
                        while (input.last() !== ' ') { // no spaces in arguments
                            args += input.pop();
                        }
                        input.pop(); // pop the space
                    }
                }
                var value = "";
                while (input.last() !== '/') { // till end of tag
                    value += input.pop();
                }
                input.pop(); // pop dangling '/'
                
                tag.push(value);
                tag.args = args;
                current.push(tag);
                prevent_push = true;
            } else {
                while (name.length) { // pushback letters
                    input.push(name[name.length - 1]);
                    name = name.substr(name.length - 1);
                }
            }
        }
        
        if (!prevent_push) {
            current.push(c);
        }
    }
    //rollback
    while (current.parent !== null) {
        current = current.parent;
    }
    
    return current.get_unwinded_contents();
};

var Magician = function() {
    this.editor = null;
    var box = document.getElementById("fast_reply");
    if (box) {
        box.style.display = "block";
        box.style.position = "fixed";
        box.style.left = "0";
        box.style.bottom = "0";
        box.style.width = "100%";
    }
    
    var editor_ids = ["fast-reply_textarea", "ed-0_textarea"];
    var that = this;
    var fast_reply = false;
    editor_ids.forEach(function(el) {
        var test = document.getElementById(el);
        if (test) {
            if (el === editor_ids[0]) { fast_reply = true; }
            that.editor = test;
        }
    });
    if (fast_reply) {
        this.editor.style.height = '17px';
    }
    if (this.editor) {
		var testing = false;
		if (testing) {
            document.getElementById("clicker").onclick = function() {
                try {
                    var parser = new Parser(that.editor.value);
                    document.getElementById("log").value = "";
                    document.getElementById("result").value = parser.get_bbCode();
                } catch (e) {
                    pretty_warning(e);
                    all_fine = false; 
                }
            }; // quick testing
		} else {
            this.editor.form.onsubmit = function() {
                return that.do_magic.apply(that, arguments);
            };
        }
    }
};

Magician.prototype.do_magic = function() {
    var all_fine = false;
    try {
        var parser = new Parser(this.editor.value);
        dic_helper.original_version = parser.text;
        dic_helper.editor = this.editor;
        this.editor.value = parser.get_bbCode();
        all_fine = (parser.warnings === 0);
    } catch (e) {
        pretty_warning(e);
    }
    return all_fine;
};

var mage = new Magician();



3 Comments On This Entry

Page 1 of 1

fromTheSprawl Icon

21 October 2012 - 10:47 PM
Interesting post. I too get tired of using the inline tag even though I often use it when I can.
0

Xupicor Icon

22 October 2012 - 09:05 AM
I too use it where I can, and now it's a breeze. ;)

After a few days of using the script though, I've found a big problem with my logic. I'll have to fix some bad code in there, but that probably won't happen today. I'm also thinking about changing the notation of /url:/ tag, making it into more general shorthand-tag... Something like {b:bolded text} and {acronym:Grand Theft Auto|GTA} in the form of {tag:[args|]text}. Yup, that would mean dropping / as the delimiter, since people actually use it when writing and it would in the end get in the way. {} on the other hand aren't used all that much. And you'd still have shorter notation than full bbCode. ;)
0

Xupicor Icon

10 November 2012 - 11:01 AM
Decided to cut out other shorthands than '', and throw in a general ''/tag*/''. I know slash isn't exactly the best character to pick, but I just like how it looks, somehow. ;) Oh, I know. I need to add enable/disable parsing, live. I'll do it tomorrow, probably. Just a few clicks away, but I don't want to dabble with it right now. ;)
0
Page 1 of 1

Trackbacks for this entry [ Trackback URL ]

There are no Trackbacks for this entry

April 2014

S M T W T F S
  12345
6789101112
131415 16 171819
20212223242526
27282930   

Recent Entries

Recent Comments

Search My Blog

0 user(s) viewing

0 Guests
0 member(s)
0 anonymous member(s)