module RpgCards {
function normalizeTag(tag: string): string {
return tag.trim().toLowerCase();
}
function splitParams(value: string): string[] {
return value.split("|").map(function (str) { return str.trim(); });
}
export class Options {
foreground_color: string;
background_color: string;
empty_color: string;
default_color: string;
default_icon: string;
default_title_size: string;
page_size: string;
page_rows: number;
page_columns: number;
card_arrangement: string;
card_size: string;
card_count: number;
icon_inline: boolean;
constructor() {
this.foreground_color = "white";
this.background_color = "white";
this.empty_color = "black";
this.default_color = "black";
this.default_icon = "ace";
this.default_title_size = "13";
this.page_size = "A4";
this.page_rows = 3;
this.page_columns = 3;
this.card_arrangement = "doublesided";
this.card_size = "25x35";
this.card_count = null;
this.icon_inline = true;
}
}
export class Card {
count: number;
title: string;
title_size: string;
color: string;
color_front: string;
color_back: string;
icon: string;
icon_front: string;
icon_back: string;
contents: string[];
tags: string[];
userData: any;
constructor() {
this.count = 1;
this.title = "New card";
this.title_size = null;
this.color = null;
this.color_front = null;
this.color_back = null;
this.icon = null;
this.icon_front = null;
this.icon_back = null;
this.contents = [];
this.tags = [];
this.userData = null;
}
static fromJSON(json: any): Card {
var result = new Card;
result.count = json.count || 1;
result.title = json.title || "";
result.title_size = json.title_size || null;
result.color = json.color || null;
result.color_front = json.color_front || null;
result.color_back = json.color_back || null;
result.icon = json.icon || null;
result.icon_front = json.icon_front || null;
result.icon_back = json.icon_back || null;
result.contents = json.contents || [];
result.tags = json.tags || [];
return result;
}
public toJSON(): any {
return {
count: this.count,
title: this.title,
title_size: this.title_size,
color: this.color,
color_front: this.color_front,
color_back: this.color_back,
icon: this.icon,
icon_front: this.icon_front,
icon_back: this.icon_back,
contents: this.contents.slice(),
tags: this.tags.slice()
}
}
public duplicate(): Card {
var result = Card.fromJSON(this.toJSON());
result.title += " (Copy)";
return result;
}
public hasTag(tag: string): boolean {
var index = this.tags.indexOf(normalizeTag(tag));
return index > -1;
}
public addTag(tag: string): void {
if (!this.hasTag(tag)) {
this.tags.push(normalizeTag(tag));
}
}
public removeTag(tag: string): void {
var ntag = normalizeTag(tag);
this.tags = this.tags.filter(function (t) {
return ntag != t;
});
}
public getTitle(options: Options): string {
return this.title || "";
}
public getTitleSize(options: Options): string {
return this.title_size || options.default_title_size || "13";
}
public getColorFront(options: Options): string {
return this.color_front || this.color || options.default_color || "black";
}
public getColorBack(options: Options): string {
return this.color_back || this.color || options.default_color || "black";
}
public getIconFront(options: Options): string {
return this.icon_front || this.icon || options.default_icon || "ace";
}
public getIconBack(options: Options): string {
return this.icon_back || this.icon || options.default_icon || "ace";
}
};
interface CardAction {
fn: string;
card: Card;
ref: Card;
}
export class CardDeck {
cards: Card[];
private _actions: CardAction[];
constructor() {
this.cards = [];
this._actions = [];
}
public toJSON(): any {
return this.cards.map((card) => card.toJSON());
}
public static fromJSON(data: any): CardDeck {
if (Array.isArray(data)) {
var result = new CardDeck;
for (var i = 0; i < data.length; ++i) {
result.cards.push(Card.fromJSON(data[i]));
}
return result;
} else {
throw new Error("Invalid data");
}
}
public addCards(cards: Card[]): void {
cards.forEach((card) => {
this._actions.push({fn:"add", card: card, ref: null});
});
}
public addNewCard(): Card {
var newCard = new Card();
this._actions.push({ fn: "add", card: newCard, ref: null });
return newCard;
}
public duplicateCard(card: Card): Card {
var newCard = card.duplicate();
this._actions.push({ fn: "add", card: newCard, ref: card });
return newCard;
}
public deleteCard(card: Card): void {
this._actions.push({ fn: "del", card: card, ref: null });
}
public commit() {
for (var i = 0; i < this._actions.length; ++i) {
var action = this._actions[i];
if (action.fn === "add") {
var index = this.cards.indexOf(action.ref);
if (index > -1) {
this.cards.splice(index + 1, 0, action.card);
} else {
this.cards.push(action.card);
}
} else if (action.fn === "del") {
var index = this.cards.indexOf(action.card);
if (index > -1) {
this.cards.splice(index, 1);
}
}
}
this._actions = [];
}
}
type ContentGeneratorFunction = (params: string[], card: Card, options: Options, ind: string, ind0: string) => string;
type CardGeneratorFunction = (card: Card, options: Options, ind: string, ind0: string) => string;
export class CardHtmlGenerator {
constructor() {
}
private _icon(src: string, ind: string, ind0: string): string {
return ind + '\n';
}
private _subtitle(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var text = params[0] || "";
return ind + '' + text + '\n';
}
private _ruler(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
return ind + '\n';
}
private _boxes(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var count = params[0] || 1;
var size = params[1] || 3;
return ind + '\n';
}
private _property(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var header = params[0] || "";
var text = params[1] || "";
var result = "";
result += ind + '\n';
result += ind + ind0 + '' + header + '
\n';
result += ind + ind0 + '' + text + '
\n';
result += ind + '\n';
return result;
}
private _description(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var header = params[0] || "";
var text = params[1] || "";
var result = "";
result += ind + '\n';
result += ind + ind0 + '' + header + '
\n';
result += ind + ind0 + '' + text + '
\n';
result += ind + '\n';
return result;
}
private _text(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var text = params[0] || "";
var result = "";
result += ind + '\n';
result += ind + ind0 + '' + text + '
\n';
result += ind + '\n';
return result;
}
private _dndstats(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var stats = ["str", "dex", "con", "int", "wis", "cha"];
var result = "";
result += ind + '\n';
return result;
}
private _bullet(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var text = params[0] || "";
return ind + '' + text + '\n';
}
private _section(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var text = params[0] || "";
return ind + '' + text + '\n';
}
private _fill(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var size = params[0] || "1";
return ind + '\n';
}
private _unknown(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
var text = params.join(' | ');
return ind + '' + text + '
\n';
}
private _empty(params: string[], card: Card, options: Options, ind: string, ind0: string) {
return '';
}
private _contents(contents: string[], card: Card, options: Options, ind: string, ind0: string): string {
var result = "";
result += ind + '\n';
result += contents.map((value) => {
var parts = splitParams(value);
var name = parts[0];
var params = parts.splice(1);
var generator: ContentGeneratorFunction = null;
switch (name) {
case "subtitle": generator = this._subtitle; break;
case "property": generator = this._property; break;
case "rule": generator = this._ruler; break;
case "ruler": generator = this._ruler; break;
case "boxes": generator = this._boxes; break;
case "description": generator = this._description; break;
case "dndstats": generator = this._dndstats; break;
case "text": generator = this._text; break;
case "bullet": generator = this._bullet; break;
case "fill": generator = this._fill; break;
case "section": generator = this._section; break;
case "disabled": generator = this._empty; break;
case "": generator = this._empty; break;
default: return this._unknown(parts, card, options, ind, ind0);
}
return generator(params, card, options, ind + ind0, ind);
}).join("\n");
result += ind + '\n';
return result;
}
private _title(card: Card, options: Options, ind: string, ind0: string): string {
var title = card.getTitle(options);
var title_size = card.getTitleSize(options)
var icon = card.getIconFront(options);
var result = "";
result += ind + '\n';
result += ind + ind0 + '' + title + '
\n';
result += ind + ind0 + '' + "" + '
\n';
result += this._icon(icon, ind + ind0, ind0);
result += ind + '\n';
return result;
}
private _card_front(card: Card, options: Options, ind: string, ind0: string): string {
var result = "";
result += this._title(card, options, ind + ind0, ind0);
result += this._contents(card.contents, card, options, ind + ind0, ind0);
return result;
}
private _card_back(card: Card, options: Options, ind: string, ind0: string): string {
var icon = card.getIconBack(options);
var result = "";
result += ind + '\n';
result += this._icon(icon, ind + ind0, ind);
result += ind + '\n';
return result;
}
private _card_empty(options: Options, ind: string, ind0: string): string {
var result = "";
result += ind + '\n';
result += ind + '\n';
return result;
}
private _card(options: Options, ind: string, ind0: string, content: string, color: string): string {
var size = options.card_size || "25x35";
var result = "";
result += ind + '\n';
result += content;
result += ind + '\n';
return result;
}
/** Generates HTML for the front side of the given card */
public card_front(card: Card, options: Options, indent: string): string {
var content = this._card_front(card, options, "", indent);
return this._card(options, "", indent, content, card.getColorFront(options));
}
/** Generates HTML for the back side of the given card */
public card_back(card: Card, options: Options, indent: string): string {
var content = this._card_back(card, options, "", indent);
return this._card(options, "", indent, content, card.getColorBack(options));
}
/** Generates HTML for an empty given card */
public card_empty(options: Options, indent: string): string {
var content = this._card_empty(options, "", indent);
return this._card(options, "", indent, content, options.empty_color);
}
}
class CardPage {
rows: number;
cols: number;
cards: T[];
constructor(rows: number, cols: number) {
this.rows = rows;
this.cols = cols;
this.cards = [];
}
/** Returns an empty page with the same dimensions */
public newPage(): CardPage {
return new CardPage(this.rows, this.cols);
}
private _posToIndex(row: number, col: number): number {
return row * this.cols + col;
}
/** Adds one card to the page */
public addCard(card: T): void {
if (this.capacity() === 0) {
throw new Error("This page is full.");
}
this.cards.push(card);
}
/**
Adds several copies of a card to the page.
Returns the number of copies that did not fit on the page.
*/
public addCards(card: T, count: number): number {
while (this.capacity() > 0 && count > 0) {
this.addCard(card);
--count;
}
return count;
}
/** Fills all remaining slots on the current row with the given card */
public fillRow(card: T): void {
while (this.capacityRow() > 0) {
this.addCard(card);
}
}
/** Fills all remaining slots on the page with empty cards */
public fillPage(card: T): void {
while (this.capacity() > 0) {
this.addCard(card);
}
}
/** Empty slots on the page */
public capacity(): number {
return this.rows * this.cols - this.cards.length;
}
/** Empty slots on the current line */
public capacityRow(): number {
return this.capacity() % this.cols;
}
/** Flip card slots horizontally */
public flipH() {
if (this.capacity() > 0) {
throw new Error("Cannot perform this operation while the page is not full");
}
for (var r = 0; r < this.rows; ++r) {
for (var c = 0; c < Math.floor(this.cols / 2); ++c) {
var indexL = this._posToIndex(r, c);
var indexR = this._posToIndex(r, this.cols - c - 1);
var cardL = this.cards[indexL];
var cardR = this.cards[indexR];
this.cards[indexL] = cardR;
this.cards[indexR] = cardL;
}
}
}
}
class CardPageSet {
rows: number;
cols: number;
pages: CardPage[];
constructor(rows: number, cols: number) {
this.rows = rows;
this.cols = cols;
this.pages = [];
}
public lastPage(): CardPage {
if (this.pages.length === 0) {
return null;
} else {
return this.pages[this.pages.length - 1];
}
}
public addPage(): CardPage {
var newPage = new CardPage(this.rows, this.cols);
this.pages.push(newPage);
return newPage;
}
/**
Adds one card to the last page.
Adds a new pages if necessary.
*/
public addCard(card: T): void {
var page = this.lastPage();
if (page === null || page.capacity() === 0) {
page = this.addPage();
}
page.addCard(card);
}
/**
Adds several copies of a card to the last page.
Adds new pages if necessary.
*/
public addCards(card: T, count: number): void {
for (var i = 0; i < count; ++i) {
this.addCard(card);
}
}
public forEach(fn: (page: CardPage)=>void) {
this.pages.forEach(fn);
}
public merge(other: CardPageSet): CardPageSet {
if (this.pages.length !== other.pages.length) {
throw new Error("This function is only for merging two equally sized page sets");
}
var result = new CardPageSet(this.rows, this.cols);
for (var i = 0; i < this.pages.length; ++i) {
result.pages.push(this.pages[i]);
result.pages.push(other.pages[i]);
}
return result;
}
}
export class PageHtmlGenerator {
indent: string;
constructor() {
this.indent = " ";
}
private _pageColor(page: number, options: Options): string {
if ((options.card_arrangement == "doublesided") && (page % 2 == 1)) {
return options.background_color;
} else {
return options.foreground_color;
}
}
private _wrap(pageSet: CardPageSet, options: Options) {
var size = options.page_size || "A4";
var result = "";
for (var i = 0; i < pageSet.pages.length; ++i) {
var page = pageSet.pages[i];
var style = 'style="background-color:' + this._pageColor(i, options) + '"';
result += '\n';
result += page.cards.join("");
result += '\n';
}
return result;
}
private _generatePagesDoublesided(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet {
var front_pages: CardPageSet = new CardPageSet(rows, cols);
var back_pages: CardPageSet = new CardPageSet(rows, cols);
var empty = generator.card_empty(options, this.indent);
// Fill pages with cards
for (var i = 0; i < cards.length; ++i) {
var card = cards[i];
var front = generator.card_front(card, options, this.indent);
var back = generator.card_back(card, options, this.indent);
front_pages.addCards(front, card.count);
back_pages.addCards(back, card.count);
}
// Fill empty slots
front_pages.forEach((page) => page.fillPage(empty));
back_pages.forEach((page) => page.fillPage(empty));
// Shuffle back cards so that they line up with their corresponding front cards
back_pages.forEach((page) => page.flipH());
// Interleave front and back pages so that we can print double-sided
return front_pages.merge(back_pages);
}
private _generatePagesFrontOnly(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet {
var pages: CardPageSet = new CardPageSet(rows, cols);
var empty = generator.card_empty(options, this.indent);
// Fill pages with cards
for (var i = 0; i < cards.length; ++i) {
var card = cards[i];
var front = generator.card_front(card, options, this.indent);
pages.addCards(front, card.count);
}
// Fill empty slots
pages.forEach((page) => page.fillPage(empty));
return pages;
}
private _generatePagesSideBySide(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet {
if (cols < 2) {
throw new Error("Need at least two columns for side-by-side");
}
var pages: CardPageSet = new CardPageSet(rows, cols);
var empty = generator.card_empty(options, this.indent);
// Fill pages with cards (two at a time)
for (var i = 0; i < cards.length; ++i) {
var card = cards[i];
var front = generator.card_front(card, options, this.indent);
var back = generator.card_back(card, options, this.indent);
for (var j = 0; j < card.count; ++j) {
if (pages.pages.length > 0 && pages.lastPage().capacityRow() < 2) {
pages.lastPage().fillRow(empty);
}
pages.addCard(front);
pages.addCard(back);
}
}
// Fill empty slots
pages.forEach((page) => page.fillPage(empty));
return pages;
}
private _generatePages(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet {
switch (options.card_arrangement) {
case "doublesided": return this._generatePagesDoublesided(cards, options, rows, cols, generator);
case "front_only": return this._generatePagesFrontOnly(cards, options, rows, cols, generator);
case "side_by_side": return this._generatePagesSideBySide(cards, options, rows, cols, generator);
default: throw new Error("Unknown card arrangement");
}
}
private _generateStyle(options) {
var size = "a4";
switch (options.page_size) {
case "A3": size = "A3 portrait"; break;
case "A4": size = "210mm 297mm"; break;
case "A5": size = "A5 portrait"; break;
case "Letter": size = "letter portrait"; break;
case "25x35": size = "2.5in 3.5in"; break;
default: size = "auto";
}
var result = "";
result += "\n";
return result;
}
public generateHtml(cards: Card[], options: Options) {
options = options || new Options();
var rows = options.page_rows || 3;
var cols = options.page_columns || 3;
// Generate the HTML for each card
var generator = new CardHtmlGenerator();
var pages: CardPageSet = this._generatePages(cards, options, rows, cols, generator);
// Wrap all pages in a element
var document = this._wrap(pages, options);
// Generate the HTML for the page layout
var style = this._generateStyle(options);
// Wrap all pages in a element and add CSS for the page size
var result = "";
result += style
result += document;
return result;
}
public insertInto(cards: Card[], options: Options, container: HTMLElement) {
// Clear the previous content of the document
while (container.hasChildNodes()) {
container.removeChild(container.lastChild);
}
// Insert the HTML
var html = this.generateHtml(cards, options);
container.innerHTML = html;
}
}
}