This library is a markup parser which is intended to be used in node.js and browser environment. I've decided to use Jasmine for tests.
The library's name is ivy-markup. URL: https://github.com/AlexAtNet/ivy-markup
It would be great to get comments on its usability and code structure.
Main engineering ideas:
- Parser parses the rest of the stream by trying several possibilities (e.g. try to parse table, then line, then characters)
- Parser creates a tree of nodes
- Rendering is two-step: node rendering and HTML tag rendering
Significant code fragments:
parser.js
// Character ranges:
(function () {
/**
* Reads whitespace characters from the stream.
*/
Parser.prototype.whitespace = function (state) {
return state.read(this.whitespaceCharacter);
};
/**
* Reads whitespace character from the stream.
*/
Parser.prototype.whitespaceCharacter = function (state) {
if (state.end() || !state.current(/[ \t]/)) {
return null;
}
return state.fetch();
};
/**
* Reads characters from the stream.
*
* Reads the current character and then calls the character method
* until it returns null.
*
* Examples (| - state position):
* " |asd, qwe. [a|...] " -> {"asd, qwe. "}, " asd, qwe. |[a|...] "
* " |e@mail.com!" -> {"e"}, " e|@mail.com!"
* " e|@mail.com!" -> {"@mail.com"}, " e@mail.com|!"
*/
Parser.prototype.characters = function (state) {
return this.createCharacterNode(
(this.character(state) || state.fetch() || '') +
(state.read(this.character) || '')
);
};
/**
* Reads character from the stream.
*
* Returns:
* 1. current character when it is uppercase or lowercase letter
* from A-Z, digit, comma, point, or white space
* 2. character after backslash when it is not from #1
* 3. backslash when it is followed by the character from #1
* 4. null when the state ends or not #1-#3.
*
* Examples (| - state position):
* a|sdf -> s, as|df
* a|\sdf -> \, a\|sdf
* a|\\sdf -> \, a\\|sdf
* a|\.sdf -> ., a\.|sdf
* asdf| -> null, asdf|
*
* Called from:
* characters
*/
Parser.prototype.character = function (state) {
if (state.end()) {
return null;
}
if (state.current(/[a-z\d\.\, ]/i)) {
return state.fetch();
}
if (state.current('\\')) {
state.move();
if (state.end() || state.current(/[a-z\d]/i)) {
return '\\';
}
return state.fetch();
}
return null;
};
}());
renderer.js
Renderer.prototype.node = function (node, callback) {
var tag = this.tag.bind(this);
switch (node.type) {
case 'plugin':
this.plugins[node.plugin.name].render(node, callback);
break;
case 'heading':
this.nodes(node.nodes, function (result) {
callback(tag('h' + node.level, { id : node.id }, result));
}.bind(this));
break;
case 'list':
this.nodes(node.nodes, function (result) {
callback(tag({
'unordered' : 'ul',
'ordered' : 'ol'
}[node.list], result));
});
break;
...
case 'table':
this.nodes(node.nodes, function (result) {
callback(tag('table', { 'class' : 'table' }, result));
});
break;
case 'table-row':
this.nodes(node.nodes, function (result) {
callback(tag('tr', result));
});
break;
...
index.js
'use strict';
/*global module, window, require */
var noConflict, ivy;
ivy = function (markup, done) {
(function (markup, done) {
var parser = new this.Parser(),
tags = new this.Tags(),
renderer = new this.Renderer(tags);
renderer.render(parser.parse(markup), done);
}.call(ivy, markup, done));
};
if (('undefined' !== typeof module) && undefined !== module.exports) {
ivy.Parser = require('./parser.js');
ivy.Tags = require('./tags.js');
ivy.Renderer = require('./tags.js');
module.exports = ivy;
} else if ('undefined' !== typeof window) {
if (undefined === window.ivy) {
noConflict = (function (previous) {
return function () {
window.ivy = previous;
return ivy;
};
}(window.ivy));
} else {
noConflict = (function (previous) {
return function () {
window.ivy = previous;
return ivy;
};
}(window.ivy()));
}
ivy.noConflict = noConflict;
ivy.Parser = window.ivy.Parser;
ivy.Tags = window.ivy.Tags;
ivy.Renderer = window.ivy.Renderer;
window.ivy = ivy;
}
this
rather thanwindow
in index.js \$\endgroup\$ – megawac Jan 24 '14 at 22:56