/**
* dedalus-dadlee.js v0.9.0
* 2013, Gustavo Di Pietro
* Licensed under the GPL license (http://www.gnu.org/licenses/gpl-2.0.html)
**/
/**
* Parse a Dedalus story in dedlee format and generate a standard Dedalus HTML-like story
* @param {jQuery} inputSource jQuery element that contains, in its body, the dedlee source
* @param {jQuery} target jQuery element where to append the output of the parsing
*/
Dedalus.prototype.parseDedlee = function (inputSource, target) {
"use strict";
/*jslint evil: true, white: true, nomen: true */
/*global $, Dedalus*/
var i,
lineCounter,
cleanSource = Dedalus.getRawContent(inputSource),
sourceLines = cleanSource.match(/[^\r\n]+/g),
notEmptyLines = removeEmptyLinesAndComments(sourceLines),
source = indentToMin(notEmptyLines),
scriptTags = ['initscript', 'beforeEveryThing', 'beforeEveryPageTurn', 'beforeEveryParagraphShown', 'afterEveryThing', 'afterEveryPageTurn', 'afterEveryParagraphShown'];
/**
* Actual function that operate the parsing based on rules and substitutions
* @return {[type]} [description]
*/
function parseBlock () {
var lineNum = 0,
relativeLineNum = 0,
line = '',
out = '',
rules = [
// Title tag. Get the first line of dedlee source and treat it like the story title
{
type : 'title',
check : function () { return lineNum === 0; },
singleLine : true,
openTag : function () { return '
' + line + ''; },
closeTag : null
},
// Initscript and before/after actions. Just add the content to the appropriate tag
{
type : 'scriptTag',
check : function () { return scriptTags.indexOf(line) !== -1; },
singleLine : false,
openTag : function () { return '<' + line + '>'; },
closeTag : function () { return '' + line + '>'; }
},
// Objects. Turn o.OBJECT_ID "OPTIONAL_INVENTORY_NAME" in
//
{
type : 'object',
check : function () { return line.ltrim().startsWith('o.'); },
singleLine : false,
openTag : function () {
var maybeInventory = (line.match(/\"(.*)\"/) || [])[1],
inventory = maybeInventory ? 'inventoryName="' + maybeInventory + '"' : '',
cleanLine = line.replace(/(\".*\")/, ''),
split = cleanLine.trim().split('.'),
objectId = split[1],
objectClass = split.length === 3 ? 'class="' + split[2] + '"' : '';
return '';
},
closeTag : function () { return ''; }
},
// Characters. Just like Objects
{
type : 'character',
check : function () { return line.ltrim().startsWith('c.'); },
singleLine : false,
openTag : function () {
var maybeInventory = (line.match(/\"(.*)\"/) || [])[1],
inventory = maybeInventory ? 'inventoryName="' + maybeInventory + '"' : '',
split = line.trim().split('.'),
characterId = split[1],
characterClass = split.length === 3 ? 'class="' + split[2] + '"' : '';
return '';
},
closeTag : function () { return ''; }
},
// Object and characted actions. Look for every line in double quotes
// and treat it the the action id, for example:
//
// "Examine"
// Handome as usual
//
// generates:
//
// Handome as usual
//
{
type : 'action',
check : function (currentRule) { return (currentRule === 'object' || currentRule === 'character') && line.ltrim().startsWith('"'); },
singleLine : false,
openTag : function () { return ''; },
closeTag : function () { return ''; }
},
// Action when clause. A string starting with "when", right after
// the beninning of an action produces the tag with its content
{
type : 'when',
check : function (currentRule) { return currentRule === 'action' && line.ltrim().startsWith('when') && relativeLineNum === 0; },
singleLine : true,
openTag : function () { return '' + line.ltrim().replace(/^when /, '') + ''; },
closeTag : null
},
// Combination actions. Turn with.OBJECT_ID in
{
type : 'with',
check : function (currentRule) { return currentRule === 'action' && line.ltrim().startsWith('with'); },
singleLine : false,
openTag : function () {
var split = line.trim().split('.'),
objectId = split[1];
return '';
},
closeTag : function () { return ""; }
},
// Pages. Turn p.PAGE_ID in
{
type : 'page',
check : function () { return line.ltrim().startsWith('p.'); },
singleLine : false,
openTag : function () {
var split = line.trim().split('.'),
pageId = split[1],
pageClass = split.length === 3 ? 'class="' + split[2] + '"' : '';
return '';
},
closeTag : function () { return ''; }
},
// Paragraphs. Turn pg.PARAGRAPH_ID in
{
type : 'paragraph',
check : function () { return line.ltrim().startsWith('pg.'); },
singleLine : false,
openTag : function () {
var split = line.trim().split('.'),
paragraphId = split[1],
paragraphClass = split.length === 3 ? 'class="' + split[2] + '"' : '';
return '';
},
closeTag : function () { return ''; }
},
// Everything else is just printed out like it is
{
type : 'other',
check : function () { return true; },
singleLine : true,
openTag : function () { return line; },
closeTag : null
}
],
substRules = [
// [[PAGE_ID]]link to page[[]] => link to page
{
applyTo : 'page, paragraph, action',
replaceRgx : /\[\[(.*)\]\](.*)\[\[\]\]/g,
withRgx : '$2'
},
// {{OBJECT_ID}}link to object{{}} => link to object
{
applyTo : 'page, paragraph, action',
replaceRgx : /\{\[(.*)\]\}(.*)\{\[\]\}/g,
withRgx : '$2'
},
// (PARAGRAPH_ID))link to paragraph() => link to paragraph
{
applyTo : 'page, paragraph, action',
replaceRgx : /\(\((.*)\)\)(.*)\(\(\)\)/g,
withRgx : '$2'
}
];
/**
* Recursive function to parse a block starting from the current line
* to recognize a block, it uses the current indentation level and
* keeps analyzing line by like till the block dedets (just like Python)
* @param {String} currentRule Parent block type (object, action, page...)
*/
function _parseBlock (currentRule) {
line = source[lineNum];
// Keep track of the line number *withing the block*
relativeLineNum = 0;
var i, rule,
closeTag = '',
initialIndentation = intendationLevel(line);
// Keep working till there are lines or code or the current block
// ends
do {
// Search the appropriate rule
for (i = 0; i < rules.length; i += 1) {
rule = rules[i];
if (rule.check.call(this, currentRule)) {
// Add the opening tag
out += rule.openTag() + '\n';
// Recursively analyze the block contained in the current
// one if it is not of type "single line"
if (!rule.singleLine) {
closeTag = rule.closeTag();
// Advance by one line to enter the contained block
// When done, go back by one line
lineNum += 1;
_parseBlock(rule.type);
lineNum -= 1;
// Close the tag
out += closeTag + '\n';
}
lineNum += 1;
relativeLineNum += 1;
line = source[lineNum];
break;
}
}
} while (line && intendationLevel(line) >= initialIndentation);
}
_parseBlock();
target.append(out);
// Withing every block of text that requires it, substitute placeholders
// for links with actual
(function () {
var substRule;
function replaceLinks(index, el) {
// Apply the replace rules to the raw content of the current element
$(el).html(Dedalus.getRawContent($(el)).replace(substRule.replaceRgx, substRule.withRgx));
// Search for , , whose attribute contains a
// dot and use it as an indicator of an id to be set. Example:
// link => link
// A third dot is treated like a class
// link => link
function setId(idx, e) {
e = $(e);
var split,
attrib = (e.attr('to') && 'to') || (e.attr('with') && 'with') || (e.attr('paragraph') && 'paragraph'),
attribVal = e.attr(attrib);
if (attribVal.indexOf('.') !== -1) {
split = attribVal.split('.');
e.attr(attrib, split[0]);
e.attr('id', split[1]);
if (split.length === 3) {
e.addClass(split[2]);
}
}
}
$(el).find('turn, show, interact').each(setId);
}
for (i = 0; i < substRules.length; i += 1) {
substRule = substRules[i];
target.find(substRule.applyTo).each(replaceLinks);
}
}());
}
parseBlock();
/* ** UTILITY FUNCTIONS ** */
/**
* Calculate the number of spaces in front of a string
* @param {String} str string to evaluate
* @return {Integer} number of spaces before the string
*/
function intendationLevel(str) {
return str.length - str.ltrim().length;
}
/**
* Given an array of strings, return only those that are not empty and not
* commented (staring with #)
* @param {Array} strings Strings to filter
* @return {Array} string without empty lines
*/
function removeEmptyLinesAndComments(strings) {
return strings.filter(function (str) {
return str.trim() !== '' && !str.trim().startsWith('#');
});
}
/**
* Find the minimum indentation level of an array of strings and align
* all of them to the lower one, for example:
* ---
* AAA
* BBB
* CCC
* DDD
* ---
* returns
* ---
* AAA
* BBB
* CCC
* DDD
* ---
*
* @param {Array} strings String to align to the left
* @return {Array} Aligned strings
*/
function indentToMin(strings) {
var minIndentation = Math.min.apply(Math,
strings.map(function (line) {
return intendationLevel(line);
}));
return strings.map(function (line) {
return line.substr(minIndentation).rtrim();
});
}
};