Skip to content

Instantly share code, notes, and snippets.

@jpdenford
Forked from dribnet/.block
Last active October 13, 2016 04:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpdenford/2a96d5b84b198671981dc055164c70cb to your computer and use it in GitHub Desktop.
Save jpdenford/2a96d5b84b198671981dc055164c70cb to your computer and use it in GitHub Desktop.
Bogus Clickbait Science
license: mit
// note: this file is poorly named - it can generally be ignored.
// helper functions below for supporting blocks/purview
function saveBlocksImages(doZoom) {
if(doZoom == null) {
doZoom = false;
}
// generate 960x500 preview.jpg of entire canvas
// TODO: should this be recycled?
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 960;
offscreenCanvas.height = 500;
var context = offscreenCanvas.getContext('2d');
// background is flat white
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 960, 500);
context.drawImage(this.canvas, 0, 0, 960, 500);
// save to browser
var downloadMime = 'image/octet-stream';
var imageData = offscreenCanvas.toDataURL('image/jpeg');
imageData = imageData.replace('image/jpeg', downloadMime);
p5.prototype.downloadFile(imageData, 'preview.jpg', 'jpg');
// generate 230x120 thumbnail.png centered on mouse
offscreenCanvas.width = 230;
offscreenCanvas.height = 120;
// background is flat white
context = offscreenCanvas.getContext('2d');
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 230, 120);
if(doZoom) {
// pixelDensity does the right thing on retina displays
var pd = this._pixelDensity;
var sx = pd * mouseX - pd * 230/2;
var sy = pd * mouseY - pd * 120/2;
var sw = pd * 230;
var sh = pd * 120;
// bounds checking - just displace if necessary
if (sx < 0) {
sx = 0;
}
if (sx > this.canvas.width - sw) {
sx = this.canvas.width - sw;
}
if (sy < 0) {
sy = 0;
}
if (sy > this.canvas.height - sh) {
sy = this.canvas.height - sh;
}
// save to browser
context.drawImage(this.canvas, sx, sy, sw, sh, 0, 0, 230, 120);
}
else {
// now scaledown
var full_width = this.canvas.width;
var full_height = this.canvas.height;
context.drawImage(this.canvas, 0, 0, full_width, full_height, 0, 0, 230, 120);
}
imageData = offscreenCanvas.toDataURL('image/png');
imageData = imageData.replace('image/png', downloadMime);
p5.prototype.downloadFile(imageData, 'thumbnail.png', 'png');
}

Bogus Science Generator

This is a twitterbot-style image generator. It generates bogus scientific headlines overlayed on a partially related image.

I originalyly wanted to create something that was related to current events or news. I moved away from the idea due to the lack of natural language APIs which could consistently pull relevant keywords from titles. I decided to instead use tracery to generate the headlines and overlay them on top of images sourced from loremflicker.

The process is as follows:

  • Generate title using tracery
  • Extract keywords using mashape text analysis api
  • Use these keywords in a loremflicker random image search
  • Overlay text on image
  • Display!

More Details

Artifact

My generator makes bogus scientific breakthrough claims. This is in the form of some text overlayed on a relevant image to represent a clickbait-style news item.

Positive Qualities include:

  1. Image related to text.
  2. A syntactically accurate sentence/statement.
  3. A funny or surprising correlation between cause and effect

Undesirable Qualities include:

  1. Malformed Sentences / bad formatting
  2. Sentences which don't make sense

Techniques used to achieve desired output. Using tracery to create sentences should give interesting and surprising results if done well. This will include tokenising / breaking down sentences and into useable sub components eg. items, animals, diseases, causes and effects.

An important consideration is to pluralise all items so as to create a general syntax which will work syntactically in most/all situations.

Limiting the grammar to compatible sub components eg. eating clothes, with #action# #thing# where the action makes some sense in relation to the thing.

Adding inputs

Initially I wanted to make the bot open through the use of sentence analysis APIs and existing news headlines. This turned out to not be feesible with most headlines due to the language used / limitations of the APIs.

I extracted the search terms using a language api to label the nouns (mashape text-api). To make the closed bot into an open one I then used an external image API - loremflickr which allowed me to query on the extracted nouns.

Contribution to discussion

vusd/p5bot#8


function bot() {
// this.url = 'https://query.yahooapis.com/v1/public/yql?q=select%20item.forecast%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22wellington%2Cnz%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys'
this.image_url = 'http://loremflickr.com/440/220';
this.weatherData;
this.temperatures = [];
this.have_drawn = false;
this.grammarSource = {};
this.grammar;
this.keywords;
// this.haveWord;
var BORDER_SIZE = 10; //text start offset from left border
var TEXT_SIZE = 20;
this.isDone = function() {
return this.have_drawn ;
}
this.preload = function() {
this.grammarSource = loadJSON('grammar.json');
}
this.setup = function() {
bot.grammar = tracery.createGrammar(this.grammarSource);
var story = this.grammar.flatten('#body#');
bot.simpleStory = story;
bot.story = splitStory(story);
bot.intro = this.grammar.flatten('#genericIntro#');
getWordData(removeUnwantedKeywords(story,this.grammarSource), function(data){
bot.keywords = data;
var keywords = bot.keywords.join(",").trim();
keywords = keywords == ""? "research,science,medicine" : keywords;
var url = bot.image_url + "/" + keywords;
bot.img = createImg(url);
console.log("Image",url);
});
}
/*Clean up keywords used to query image api to remove unwanted*/
function removeUnwantedKeywords(words, grammarSource){
// console.log("Unwanted words",grammarSource);
// var unwantedWords = bot.grammarSource.body.join(",").slice(0);
// console.log(unwantedWords);
// unwantedWords = unwantedWords.replaceAll('#','').split(' ');
// unwantedWords.map(function(o){
// return o[0].toLowerCase() + o.slice(1);
// });
// console.log(unwantedWords);
return words;
}
function splitStory(story){
var maxLength = 44;
var s = [];
if(story.length < maxLength){
s = [story];
} else {
s = splitLinesToFit(story, maxLength);
}
return s;
}
/*Recursively split the text until no more will fit on each line */
function splitLinesToFit(story, maxLength){
return _splitLinesToFit([""], story.split(" "), maxLength);
}
/* helper function */
function _splitLinesToFit(lines, rest, maxLength){
if(rest.length === 0) return lines;
var nextWord = rest[0];
if(lines[lines.length - 1].length + nextWord.length > maxLength){
lines.push("");
return _splitLinesToFit(lines, rest, maxLength);
} else {
lines[lines.length - 1] += (nextWord + " ");
return _splitLinesToFit(lines, rest.slice(1), maxLength);
}
}
this.respond = function() {
if(!this.img || this.img.width <= 0 || !this.keywords) {
text("loading", 100, 100);
return "loading";
}
$('img').hide();
var top = 10;
var line_space = 10;
image(this.img, 0, 0, 440, 220);
textFont('Oswald');
textBackground(BORDER_SIZE, top, textWidth(this.intro) + 5, TEXT_SIZE + line_space);
stroke(255);
strokeWeight(1);
textSize(TEXT_SIZE);
text(this.intro, top, 30);
for (var i = 0; i < this.story.length; i++) {
var line = this.story[i]; //trim
var yPos = (top + TEXT_SIZE) + (i+1) * (TEXT_SIZE + line_space);
textBackground(BORDER_SIZE, yPos - TEXT_SIZE, textWidth(line) + 5, TEXT_SIZE + line_space);
fill(0);
stroke(255);
text(line, BORDER_SIZE, yPos);
}
var message = bot.intro + " " + bot.simpleStory + ".";
return message;
}
function textBackground(x, y, wid, hei){
push();
noStroke();
fill(255,150);
rect(x, y, wid, hei);
pop();
}
}
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
/*Retreive data Nouns from text api*/
function getWordData(sentence, cb){
$.ajax({
url: "https://textapis.p.mashape.com/tags/",
data: "text="+sentence,
crossDomain: true,
context: document.body,
headers: {
"X-Mashape-Key": "5j0QV388dfmshndBCnMmdw0lgtlkp1S6yT5jsn2YUYSgZ0fokt",
"Accept": "application/json"
}
}).done(myCb);
function myCb(data) {
cb(filterTags(data));
}
function filterTags(data){
var nouns = [];
data.forEach(function(o){
if(o[1].indexOf("NN") != -1){
nouns.push(o[0]);
}
})
return nouns;
}
}
function resetFocusedRandom() {
return Math.seedrandom(arguments);
}
function focusedRandom(min, max, focus, mean) {
// console.log("hello")
if(max === undefined) {
max = min;
min = 0;
}
if(focus === undefined) {
focus = 1.0;
}
if(mean === undefined) {
mean = (min + max) / 2.0;
}
if(focus == 0) {
return d3.randomUniform(min, max)();
}
else if(focus < 0) {
focus = -1 / focus;
}
sigma = (max - mean) / focus;
val = d3.randomNormal(mean, sigma)();
if (val > min && val < max) {
return val;
}
return d3.randomUniform(min, max)();
}
{
"origin": ["#genericIntro# #body#"],
"body":["Science Links #illness# To #cause#","#cause# Can Now Cause #illness#","Research Proves #illness# Actually Due To #cause#","#animal# #frequency# #swapAforB# #item# for #item#"],
"genericIntro":["Shocking:","Latest Discovery:","Statistical Breakthrough:","Unbelievable:","Conclusive Evidence:"],
"intro":["New Study Reveals How","Forensic Evidence Suggests","Scientists Stunned As"],
"swapAforB":["Mistake","Confuse","Swap","Trade","Try To Steal Then Swap"],
"frequency":["Sometimes","Often","Rarely","Never","Seldom","Always"],
"how":["Correctly","Incorrectly",""],
"illness" :["#disease#","#adjective# #bodypart#"],
"disease":["Acne","Acute Bronchitis","AIDS","Allergy","Alopecia","Altitude Sickness","Alzheimer's Disease","Andropause","Anorexia Nervosa","Antisocial Personality Disorder","Arthritis","Asperger Syndrome","Asthma","Attention Deficit Hyperactivity Disorder","Autism","Avoidant Personality Disorder","Back Pain","Bad Breath","Benign Prostatic Hyperplasia","Bipolar Disorder","Bladder Cancer","Hemorrhage","Body Dysmorphic Disorder","Bone Cancer","Borderline Personality Disorder","Bovine Spongiform Encephalopathy","Brain Cancer","Brain Tumor","Breast Cancer","Burns","Bursitis","Cancer","Candidiasis","Canker Sores","Cardiovascular Disease","Carpal Tunnel Syndrome","Celiac Disease","Cervical Cancer","Cholesterol","Chronic Childhood Arthritis","Chronic Obstructive Pulmonary Disease","Colorectal Cancer","Conjunctivitis","Cradle Cap","Crohn's Disease","Dandruff","Deafness","Deep Vein Thrombosis","Dehydration","Dependent Personality Disorder","Gastroesophageal Reflux Disease","Acne","Allergy","Antisocial Personality Disorder","Attention Deficit Hyperactivity Disorder","Altitude Sickness","Alzheimer's Disease","Andropause","Anorexia Nervosa","Arthritis","Asperger Syndrome","Asthma","Autism","Avoidant Personality Disorder","Back Pain","Bad Breath","Alopecia","Bedwetting","Bipolar Disorder","Bladder Cancer","Body Dysmorphic Disorder","Borderline Personality Disorder","Bone Cancer","Brain Cancer","Breast Cancer","Brain Tumor","Traumatic Brain Injury","Acute Bronchitis","Burns","Bursitis","Cancer","Canker Sores","Carpal Tunnel Syndrome","Celiac Disease","Cervical Cancer","Cholesterol","Chronic Obstructive Pulmonary Disease","Colorectal Cancer","Heart Failure","Cradle Cap","Crohn's Disease","Dandruff","Deep Vein Thrombosis","Dehydration","Dependent Personality Disorder","Depression","Diabetes Mellitus","Irritant Diaper Dermatitis","Diarrhea","Disabilities","Diverticulitis","Down Syndrome","Drug Abuse","Dysfunctional Uterine Bleeding","Dyslexia","Ear Infections","Ear Problems","Eating Disorders","Dermatitis","Endometriosis","Benign Prostatic Hyperplasia","Epilepsy","Erectile Dysfunction","Eye Problems","Fibromyalgia","Fracture","Freckle","Flu","Gallbladder Disease","Gallstone","Generalized Anxiety Disorder","Herpes Simplex","Genital Wart","Glomerulonephritis","Gonorrhea","Gout","Gum Diseases","Gynecomastia","Head Lice","Headache","Deafness","Myocardial Infarction","Cardiovascular Disease","Heartburn","Heat Stroke","Heel Pain","Hemorrhage","Hemorrhoids","Hepatitis","Herniated Discs","Hiatal Hernia","Histrionic Personality Disorder","AIDS","HIV","Hives","Hyperglycemia","Hyperkalemia","Hypertension","Hyperthyroidism","Hypothyroidism"],
"adjective":["Big","Large","Small","Long","Thin","Elongated"],
"bodypart":["Noses","Eyes","Hair","Nostrils","Thumbs","Legs"],
"feeling":["Enjoy","Hate","Dislike","Abhor","Love"],
"animal":["Cats","Dogs","Chipmunks","Ostriches","Owls","Bison","Dolphins","Ponies","Apes","Cows","Ducks","Sharks","Birds","Horses","Fish"],
"cause" : ["#how# #activity# #item#"],
"activity":["Eating", "Using Your", "Using", "Playing With", "Disposing Of", "Thoroughly Cleaning"],
"item": ["Apples","Bags","Balloons","Bananas","Beds","Beef","Blouses","Books","Boom Boxes","Bottles","Bottle Caps","Bows","Bowls","Boxes","Bracelets","Bread","Brocolli","Hair Brushes","Buckel","Buttons","Cameras","Candles","Candy Wrappers","Canvas","Cars","Greeting Cards","Playing Cards","Carrots","Cats","CDs","Cell Phones","Packing Peanuts","Cinder Blocks","Chairs","Chalk","Newspapers","Soy Sauce Packets","Chapter Books","Checkbooks","Chocolate","Clay Pots","Clocks","Clothes","Computers","Conditioner","Cork","Couches","Credit Cards","Cups","Deodorant","Desks","Doors","Drawers","Drill Press","Erasers","Eye Liner","Face Wash","Fake Flowers","Flags","Flowers","Food","Forks","Fridges","Glasses","Glow Sticks","Grid Papers","Hair Ties","Clothes Hangers","Helmets","Houses","Ipods","Chargers","Key Chains","Keyboards","Keys","Knifes","Laces","Lamps","Lamp Shades","Leg Warmers","Lip Gloss","Lotion","Milk","Mirrors","Model Cars","Money","Monitors","Mops","Mouse Pads","Mp3 Players","Nail Clippers","Nail Files","Needles","Paint Brushs","Pants","Paper","Pens","Pencils","Perfume","Phones","Photo Albums","Picture Frames","Pillows","Plastic Forks","Plates","Pool Sticks","Soda Cans","Puddles","Purses","Blankets","Radios","Remotes","Rings","Rubber Bands","Rubber Ducks","Rugs","Rusty Nails","Sailboats","Sand Paper","Sandals","Scotch Tape","Screws","Seat Belts","Shampoo","Sharpies","Shawls","Shirts","Shoe Laces","Shoes","Shovels","Sketch Pads","Slippers","Soap","Socks","Sofas","Speakers","Sponges","Spoons","Springs","Sticky Notes","Stockings","Stop Signs","Street Lights","Sun Glasses","Tables","Teddies","Televisions","Thermometers","Tire Swings","Tissue Boxs","Toe Rings","Toilets","Tomatos","Tooth Picks","Toothbrushes","Toothpaste","Towels","Trees","Trucks","TVs","Tweezers","Vases","Video Games","Wallets","Washing Machines","Watches","Water Bottles","Dolls","Magnets","Wagons","Headphones","Clamps","USB Drives","Air Fresheners","Pianos","Ice Cube Trays","Windows","Coasters","Thermostats","Zippers"],
"olditems": ["Cellphones","Sausages","Eyes","Desks","Gloves","Computers","Cars","Limbs","Porridge","Chainsaws","Platypus'","Donkies","Houses"]
}
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.3/p5.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.3/addons/p5.dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.0/seedrandom.min.js"></script>
<script src="https://d3js.org/d3-random.v1.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script defer src="tracery.js"></script>
<link href="https://fonts.googleapis.com/css?family=Oswald" rel="stylesheet">
<script language="javascript" type="text/javascript" src="focusedRandom.js"></script>
<script language="javascript" type="text/javascript" src=".purview_helper.js"></script>
<script language="javascript" type="text/javascript" src="bot.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<style>
body {padding: 0; margin: 0;}
ul li {
list-style:none;
overflow:hidden;
border:1px solid #dedede;
margin:5px;
padding:5px;
}
.media img {
max-width:440px;
max-height:220px;
}
</style>
</head>
<body style="background-color:white">
<div id="canvasContainer"></div>
<pre>
<p id="tweet_text">
</p>
</pre>
<hr>
<div id="tweetExamples"></div>
</body>
var rndSeed;
var bot;
var renderReady = false;
function preload() {
bot = new bot();
bot.preload();
}
function setup () {
var main_canvas = createCanvas(440, 220);
main_canvas.parent('canvasContainer');
rndSeed = random(1024);
bot.setup();
}
function keyTyped() {
if (key == '!') {
saveBlocksImages();
}
else if (key == '@') {
saveBlocksImages(true);
}
}
function reportRenderReady() {
finalDiv = createDiv('(render ready)');
finalDiv.id("render_ready")
}
function draw() {
background(204);
// randomSeed(0);
resetFocusedRandom(rndSeed);
message = bot.respond();
var text = select('#tweet_text');
text.html(message);
if(renderReady == false) {
if(bot.isDone()) {
reportRenderReady();
renderReady = true;
}
}
}
/**
* @author Kate Compton
*/
var tracery = {
utilities : {}
};
(function() {
function inQuotes(s) {
return '"' + s + '"';
};
function parseAction(action) {
return action;
};
// tag format
// a thing to expand, plus actions
function parseTag(tag) {
var errors = [];
var prefxns = [];
var postfxns = [];
var lvl = 0;
var start = 0;
var inPre = true;
var symbol,
mods;
function nonAction(end) {
if (start !== end) {
var section = tag.substring(start, end);
if (!inPre) {
errors.push("multiple possible expansion symbols in tag!" + tag);
} else {
inPre = false;
var split = section.split(".");
symbol = split[0];
mods = split.slice(1, split.length);
}
}
start = end;
};
for (var i = 0; i < tag.length; i++) {
var c = tag.charAt(i);
switch(c) {
case '[':
if (lvl === 0) {
nonAction(i);
}
lvl++;
break;
case ']':
lvl--;
if (lvl === 0) {
var section = tag.substring(start + 1, i);
if (inPre)
prefxns.push(parseAction(section));
else
postfxns.push(parseAction(section));
start = i + 1;
}
break;
default:
if (lvl === 0) {
}
break;
}
}
nonAction(i);
if (lvl > 0) {
var error = "Too many '[' in rule " + inQuotes(tag);
errors.push(error);
}
if (lvl < 0) {
var error = "Too many ']' in rule " + inQuotes(tag);
errors.push(error);
}
return {
preActions : prefxns,
postActions : postfxns,
symbol : symbol,
mods : mods,
raw : tag,
errors : errors,
};
};
// Split a rule into sections
function parseRule(rule) {
var sections = [];
var errors = [];
if (!( typeof rule == 'string' || rule instanceof String)) {
errors.push("Cannot parse non-string rule " + rule);
sections.errors = errors;
return sections;
}
if (rule.length === 0) {
return [];
}
var lvl = 0;
var start = 0;
var inTag = false;
function createSection(end) {
var section = rule.substring(start, end);
if (section.length > 0) {
if (inTag)
sections.push(parseTag(section));
else
sections.push(section);
}
inTag = !inTag;
start = end + 1;
}
for (var i = 0; i < rule.length; i++) {
var c = rule.charAt(i);
switch(c) {
case '[':
lvl++;
break;
case ']':
lvl--;
break;
case '#':
if (lvl === 0) {
createSection(i);
}
break;
default:
break;
}
}
if (lvl > 0) {
var error = "Too many '[' in rule " + inQuotes(rule);
errors.push(error);
}
if (lvl < 0) {
var error = "Too many ']' in rule " + inQuotes(rule);
errors.push(error);
}
if (inTag) {
var error = "Odd number of '#' in rule " + inQuotes(rule);
errors.push(error);
}
createSection(rule.length);
sections.errors = errors;
return sections;
};
function testParse(rule, shouldFail) {
console.log("-------");
console.log("Test parse rule: " + inQuotes(rule) + " " + shouldFail);
var parsed = parseRule(rule);
if (parsed.errors && parsed.errors.length > 0) {
for (var i = 0; i < parsed.errors.length; i++) {
console.log(parsed.errors[i]);
}
}
}
function testParseTag(tag, shouldFail) {
console.log("-------");
console.log("Test parse tag: " + inQuotes(tag) + " " + shouldFail);
var parsed = parseTag(tag);
if (parsed.errors && parsed.errors.length > 0) {
for (var i = 0; i < parsed.errors.length; i++) {
console.log(parsed.errors[i]);
}
}
}
tracery.testParse = testParse;
tracery.testParseTag = testParseTag;
tracery.parseRule = parseRule;
tracery.parseTag = parseTag;
function spacer(size) {
var s = "";
for (var i = 0; i < size * 3; i++) {
s += " ";
}
return s;
}
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
function extend(destination, source) {
for (var k in source) {
if (source.hasOwnProperty(k)) {
destination[k] = source[k];
}
}
return destination;
}
// Inspired by base2 and Prototype
(function() {
var initializing = false,
fnTest = /xyz/.test(function() { xyz;
}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function() {
};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) {
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) : prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if (!initializing && this.init)
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();
/**
* @author Kate
*/
var Rule = function(raw) {
this.raw = raw;
this.sections = parseRule(raw);
};
Rule.prototype.getParsed = function() {
if (!this.sections)
this.sections = parseRule(raw);
return this.sections;
};
Rule.prototype.toString = function() {
return this.raw;
};
Rule.prototype.toJSONString = function() {
return this.raw;
};
/**
* @author Kate
*/
var RuleWeighting = Object.freeze({
RED : 0,
GREEN : 1,
BLUE : 2
});
var RuleSet = function(rules) {
// is the rules obj an array? A RuleSet, or a string?
if (rules.constructor === Array) {
// make a copy
rules = rules.slice(0, rules.length);
} else if (rules.prototype === RuleSet) {
// clone
} else if ( typeof rules == 'string' || rules instanceof String) {
var args = Array.prototype.slice.call(arguments);
rules = args;
} else {
console.log(rules);
throw ("creating ruleset with unknown object type!");
}
// create rules and their use counts
this.rules = rules;
this.parseAll();
this.uses = [];
this.startUses = [];
this.totalUses = 0;
for (var i = 0; i < this.rules.length; i++) {
this.uses[i] = 0;
this.startUses[i] = this.uses[i];
this.totalUses += this.uses[i];
}
};
//========================================================
// Iterating over rules
RuleSet.prototype.parseAll = function(fxn) {
for (var i = 0; i < this.rules.length; i++) {
if (this.rules[i].prototype !== Rule)
this.rules[i] = new Rule(this.rules[i]);
}
};
//========================================================
// Iterating over rules
RuleSet.prototype.mapRules = function(fxn) {
return this.rules.map(function(rule, index) {
return fxn(rule, index);
});
};
RuleSet.prototype.applyToRules = function(fxn) {
for (var i = 0; i < this.rules.length; i++) {
fxn(this.rules[i], i);
}
};
//========================================================
RuleSet.prototype.get = function() {
var index = this.getIndex();
return this.rules[index];
};
RuleSet.prototype.getRandomIndex = function() {
return Math.floor(this.uses.length * Math.random());
};
RuleSet.prototype.getIndex = function() {
// Weighted distribution
// Imagine a bar of length 1, how to divide the length
// s.t. a random dist will result in the dist we want?
var index = this.getRandomIndex();
// What if the uses determine the chance of rerolling?
var median = this.totalUses / this.uses.length;
var count = 0;
while (this.uses[index] > median && count < 20) {
index = this.getRandomIndex();
count++;
}
// reroll more likely if index is too much higher
return index;
};
RuleSet.prototype.decayUses = function(pct) {
this.totalUses = 0;
for (var i = 0; i < this.uses; i++) {
this.uses[index] *= 1 - pct;
this.totalUses += this.uses[index];
}
};
RuleSet.prototype.testRandom = function() {
console.log("Test random");
var counts = [];
for (var i = 0; i < this.uses.length; i++) {
counts[i] = 0;
}
var testCount = 10 * this.uses.length;
for (var i = 0; i < testCount; i++) {
var index = this.getIndex();
this.uses[index] += 1;
counts[index]++;
this.decayUses(.1);
}
for (var i = 0; i < this.uses.length; i++) {
console.log(i + ":\t" + counts[i] + " \t" + this.uses[i]);
}
};
RuleSet.prototype.getSaveRules = function() {
var jsonRules = this.rules.map(function(rule) {
return rule.toJSONString();
});
return jsonRules;
};
/**
* @author Kate Compton
*/
var Action = function(node, raw) {
this.node = node;
this.grammar = node.grammar;
this.raw = raw;
};
Action.prototype.activate = function() {
var node = this.node;
node.actions.push(this);
// replace any hashtags
this.amended = this.grammar.flatten(this.raw);
var parsed = parseTag(this.amended);
var subActionRaw = parsed.preActions;
if (subActionRaw && subActionRaw.length > 0) {
this.subactions = subActionRaw.map(function(action) {
return new Action(node, action);
});
}
if (parsed.symbol) {
var split = parsed.symbol.split(":");
if (split.length === 2) {
this.push = {
symbol : split[0],
// split into multiple rules
rules : split[1].split(","),
};
// push
node.grammar.pushRules(this.push.symbol, this.push.rules);
} else
throw ("Unknown action: " + parsed.symbol);
}
if (this.subactions) {
for (var i = 0; i < this.subactions.length; i++) {
this.subactions[i].activate();
}
}
};
Action.prototype.deactivate = function() {
if (this.subactions) {
for (var i = 0; i < this.subactions.length; i++) {
this.subactions[i].deactivate();
}
}
if (this.push) {
this.node.grammar.popRules(this.push.symbol, this.push.rules);
}
};
/**
* @author Kate Compton
*/
var isConsonant = function(c) {
c = c.toLowerCase();
switch(c) {
case 'a':
return false;
case 'e':
return false;
case 'i':
return false;
case 'o':
return false;
case 'u':
return false;
}
return true;
};
function endsWithConY(s) {
if (s.charAt(s.length - 1) === 'y') {
return isConsonant(s.charAt(s.length - 2));
}
return false;
};
var universalModifiers = {
capitalizeAll : function(s) {
return s.replace(/(?:^|\s)\S/g, function(a) {
return a.toUpperCase();
});
},
capitalize : function(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
},
inQuotes : function(s) {
return '"' + s + '"';
},
comma : function(s) {
var last = s.charAt(s.length - 1);
if (last === ",")
return s;
if (last === ".")
return s;
if (last === "?")
return s;
if (last === "!")
return s;
return s + ",";
},
beeSpeak : function(s) {
// s = s.replace("s", "zzz");
s = s.replace(/s/, 'zzz');
return s;
},
a : function(s) {
if (!isConsonant(s.charAt()))
return "an " + s;
return "a " + s;
},
s : function(s) {
var last = s.charAt(s.length - 1);
switch(last) {
case 'y':
// rays, convoys
if (!isConsonant(s.charAt(s.length - 2))) {
return s + "s";
}
// harpies, cries
else {
return s.slice(0, s.length - 1) + "ies";
}
break;
// oxen, boxen, foxen
case 'x':
return s.slice(0, s.length - 1) + "xen";
case 'z':
return s.slice(0, s.length - 1) + "zes";
case 'h':
return s.slice(0, s.length - 1) + "hes";
default:
return s + "s";
};
},
ed : function(s) {
var index = s.indexOf(" ");
var s = s;
var rest = "";
if (index > 0) {
rest = s.substring(index, s.length);
s = s.substring(0, index);
}
var last = s.charAt(s.length - 1);
switch(last) {
case 'y':
// rays, convoys
if (isConsonant(s.charAt(s.length - 2))) {
return s.slice(0, s.length - 1) + "ied" + rest;
}
// harpies, cries
else {
return s + "ed" + rest;
}
break;
case 'e':
return s + "d" + rest;
break;
default:
return s + "ed" + rest;
};
}
};
/**
* @author Kate Compton
*/
// A tracery expansion node
var nodeCount = 0;
var ExpansionNode = Class.extend({
init : function() {
this.depth = 0;
this.id = nodeCount;
nodeCount++;
this.childText = "[[UNEXPANDED]]";
},
setParent : function(parent) {
if (parent) {
this.depth = parent.depth + 1;
this.parent = parent;
this.grammar = parent.grammar;
}
},
expand : function() {
// do nothing
return "???";
},
expandChildren : function() {
if (this.children) {
this.childText = "";
for (var i = 0; i < this.children.length; i++) {
this.children[i].expand();
this.childText += this.children[i].finalText;
}
this.finalText = this.childText;
}
},
createChildrenFromSections : function(sections) {
var root = this;
this.children = sections.map(function(section) {
if ( typeof section == 'string' || section instanceof String) {
// Plaintext
return new TextNode(root, section);
} else {
return new TagNode(root, section);
}
});
}
});
var RootNode = ExpansionNode.extend({
init : function(grammar, rawRule) {
this._super();
this.grammar = grammar;
this.parsedRule = parseRule(rawRule);
},
expand : function() {
var root = this;
this.createChildrenFromSections(this.parsedRule);
// expand the children
this.expandChildren();
},
});
var TagNode = ExpansionNode.extend({
init : function(parent, parsedTag) {
this._super();
if (!(parsedTag !== null && typeof parsedTag === 'object')) {
if ( typeof parsedTag == 'string' || parsedTag instanceof String) {
console.warn("Can't make tagNode from unparsed string!");
parsedTag = parseTag(parsedTag);
} else {
console.log("Unknown tagNode input: ", parsedTag);
throw ("Can't make tagNode from strange tag!");
}
}
this.setParent(parent);
$.extend(this, parsedTag);
},
expand : function() {
if (tracery.outputExpansionTrace)
console.log(r.sections);
this.rule = this.grammar.getRule(this.symbol);
this.actions = [];
// Parse the rule if it hasn't been already
this.createChildrenFromSections(this.rule.getParsed());
// Do any pre-expansion actions!
for (var i = 0; i < this.preActions.length; i++) {
var action = new Action(this, this.preActions[i]);
action.activate();
}
// Map each child section to a node
if (!this.rule.sections)
console.log(this.rule);
this.expandChildren();
for (var i = 0; i < this.actions.length; i++) {
this.actions[i].deactivate();
}
this.finalText = this.childText;
for (var i = 0; i < this.mods.length; i++) {
this.finalText = this.grammar.applyMod(this.mods[i], this.finalText);
}
},
toLabel : function() {
return this.symbol;
},
toString : function() {
return "TagNode '" + this.symbol + "' mods:" + this.mods + ", preactions:" + this.preActions + ", postactions" + this.postActions;
}
});
var TextNode = ExpansionNode.extend({
isLeaf : true,
init : function(parent, text) {
this._super();
this.setParent(parent);
this.text = text;
this.finalText = text;
},
expand : function() {
// do nothing
},
toLabel : function() {
return this.text;
}
});
/**
* @author Kate Compton
*/
function Symbol(grammar, key) {
this.grammar = grammar;
this.key = key;
this.currentRules = undefined;
this.ruleSets = [];
};
Symbol.prototype.loadFrom = function(rules) {
rules = this.wrapRules(rules);
this.baseRules = rules;
this.ruleSets.push(rules);
this.currentRules = this.ruleSets[this.ruleSets.length - 1];
};
//========================================================
// Iterating over rules
Symbol.prototype.mapRules = function(fxn) {
return this.currentRules.mapRules(fxn);
};
Symbol.prototype.applyToRules = function(fxn) {
this.currentRules.applyToRules(fxn);
};
//==================================================
// Rule pushpops
Symbol.prototype.wrapRules = function(rules) {
if (rules.prototype !== RuleSet) {
if (Array.isArray(rules)) {
return new RuleSet(rules);
} else if ( typeof rules == 'string' || rules instanceof String) {
return new RuleSet(rules);
} else {
throw ("Unknown rules type: " + rules);
}
}
// already a ruleset
return rules;
};
Symbol.prototype.pushRules = function(rules) {
rules = this.wrapRules(rules);
this.ruleSets.push(rules);
this.currentRules = this.ruleSets[this.ruleSets.length - 1];
};
Symbol.prototype.popRules = function() {
var exRules = this.ruleSets.pop();
if (this.ruleSets.length === 0) {
//console.warn("No more rules for " + this + "!");
}
this.currentRules = this.ruleSets[this.ruleSets.length - 1];
};
// Clear everything and set the rules
Symbol.prototype.setRules = function(rules) {
rules = this.wrapRules(rules);
this.ruleSets = [rules];
this.currentRules = rules;
};
Symbol.prototype.addRule = function(rule) {
this.currentRules.addRule(seed);
};
//========================================================
// selection
Symbol.prototype.select = function() {
this.isSelected = true;
};
Symbol.prototype.deselect = function() {
this.isSelected = false;
};
//==================================================
// Getters
Symbol.prototype.getRule = function(seed) {
return this.currentRules.get(seed);
};
//==================================================
Symbol.prototype.toString = function() {
return this.key + ": " + this.currentRules + "(overlaying " + (this.ruleSets.length - 1) + ")";
};
Symbol.prototype.toJSON = function() {
var rules = this.baseRules.rules.map(function(rule) {
return '"' + rule.raw + '"';
});
return '"' + this.key + '"' + ": [" + rules.join(", ") + "]";
};
Symbol.prototype.toHTML = function(useSpans) {
var keySpan = '"' + this.key + '"';
if (useSpans)
keySpan = "<span class='symbol symbol_" + this.key + "'>" + keySpan + "</span>";
var rules = this.baseRules.rules.map(function(rule) {
// replace any anglebrackets for html
var cleaned = rule.raw.replace(/&/g, "&amp;");
cleaned = cleaned.replace(/>/g, "&gt;");
cleaned = cleaned.replace(/</g, "&lt;");
var s = '"' + cleaned + '"';
if (useSpans)
s = "<span class='rule'>" + s + "</span>";
return s;
});
return keySpan + ": [" + rules.join(", ") + "]";
};
/**
* @author Kate Compton
*/
function Grammar() {
this.clear();
};
Grammar.prototype.clear = function() {
// Symbol library
this.symbols = {};
this.errors = [];
// Modifier library
this.modifiers = {};
// add the universal mods
for (var mod in universalModifiers) {
if (universalModifiers.hasOwnProperty(mod))
this.modifiers[mod] = universalModifiers[mod];
}
};
//========================================================
// Loading
Grammar.prototype.loadFrom = function(obj) {
var symbolSrc;
this.clear();
if (obj.symbols !== undefined) {
symbolSrc = obj.symbols;
} else {
symbolSrc = obj;
}
// get all json keys
var keys = Object.keys(symbolSrc);
this.symbolNames = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
this.symbolNames.push(key);
this.symbols[key] = new Symbol(this, key);
this.symbols[key].loadFrom(symbolSrc[key]);
}
};
Grammar.prototype.toHTML = function(useSpans) {
// get all json keys
var keys = Object.keys(this.symbols);
this.symbolNames = [];
var lines = [];
var count = 0;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var symbol = this.symbols[key];
if (symbol && symbol.baseRules) {
lines.push(" " + this.symbols[key].toHTML(useSpans));
}
};
var s;
s = lines.join(",</p><p>");
s = "{<p>" + s + "</p>}";
return s;
};
Grammar.prototype.toJSON = function() {
// get all json keys
var keys = Object.keys(this.symbols);
this.symbolNames = [];
var lines = [];
var count = 0;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var symbol = this.symbols[key];
if (symbol && symbol.baseRules) {
lines.push(" " + this.symbols[key].toJSON());
}
};
var s;
s = lines.join(",\n");
s = "{\n" + s + "\n}";
return s;
};
//========================================================
// selection
Grammar.prototype.select = function() {
this.isSelected = true;
};
Grammar.prototype.deselect = function() {
this.isSelected = false;
};
//========================================================
// Iterating over symbols
Grammar.prototype.mapSymbols = function(fxn) {
var symbols = this.symbols;
return this.symbolNames.map(function(name) {
return fxn(symbols[name], name);
});
};
Grammar.prototype.applyToSymbols = function(fxn) {
for (var i = 0; i < this.symbolNames.length; i++) {
var key = this.symbolNames[i];
fxn(this.symbols[key], key);
}
};
//========================================================
Grammar.prototype.addOrGetSymbol = function(key) {
if (this.symbols[key] === undefined)
this.symbols[key] = new Symbol(key);
return this.symbols[key];
};
Grammar.prototype.pushRules = function(key, rules) {
var symbol = this.addOrGetSymbol(key);
symbol.pushRules(rules);
};
Grammar.prototype.popRules = function(key, rules) {
var symbol = this.addOrGetSymbol(key);
var popped = symbol.popRules();
if (symbol.ruleSets.length === 0) {
// remove symbol
this.symbols[key] = undefined;
}
};
Grammar.prototype.applyMod = function(modName, text) {
if (!this.modifiers[modName]) {
console.log(this.modifiers);
throw ("Unknown mod: " + modName);
}
return this.modifiers[modName](text);
};
//============================================================
Grammar.prototype.getRule = function(key, seed) {
var symbol = this.symbols[key];
if (symbol === undefined) {
var r = new Rule("{{" + key + "}}");
r.error = "Missing symbol " + key;
return r;
}
var rule = symbol.getRule();
if (rule === undefined) {
var r = new Rule("[" + key + "]");
console.log(r.sections);
r.error = "Symbol " + key + " has no rule";
return r;
}
return rule;
};
//============================================================
// Expansions
Grammar.prototype.expand = function(raw) {
// Start a new tree
var root = new RootNode(this, raw);
root.expand();
return root;
};
Grammar.prototype.flatten = function(raw) {
// Start a new tree
var root = new RootNode(this, raw);
root.expand();
return root.childText;
};
//===============
Grammar.prototype.analyze = function() {
this.symbolNames = [];
for (var name in this.symbols) {
if (this.symbols.hasOwnProperty(name)) {
this.symbolNames.push(name);
}
}
// parse every rule
for (var i = 0; i < this.symbolNames.length; i++) {
var key = this.symbolNames[i];
var symbol = this.symbols[key];
// parse all
for (var j = 0; j < symbol.baseRules.length; j++) {
var rule = symbol.baseRules[j];
rule.parsed = tracery.parse(rule.raw);
// console.log(rule);
}
}
};
Grammar.prototype.selectSymbol = function(key) {
console.log(this);
var symbol = this.get(key);
};
/**
* @author Kate Compton
*/
tracery.createGrammar = function(obj) {
var grammar = new Grammar();
grammar.loadFrom(obj);
return grammar;
};
tracery.test = function() {
console.log("==========================================");
console.log("test tracery");
// good
tracery.testParse("", false);
tracery.testParse("fooo", false);
tracery.testParse("####", false);
tracery.testParse("#[]#[]##", false);
tracery.testParse("#someSymbol# and #someOtherSymbol#", false);
tracery.testParse("#someOtherSymbol.cap.pluralize#", false);
tracery.testParse("#[#do some things#]symbol.mod[someotherthings[and a function]]#", false);
tracery.testParse("#[fxn][fxn][fxn[subfxn]]symbol[[fxn]]#", false);
tracery.testParse("#[fxn][#fxn#][fxn[#subfxn#]]symbol[[fxn]]#", false);
tracery.testParse("#hero# ate some #color# #animal.s#", false);
tracery.testParseTag("[action]symbol.mod1.mod2[postAction]", false);
// bad
tracery.testParse("#someSymbol# and #someOtherSymbol", true);
tracery.testParse("#[fxn][fxn][fxn[subfxn]]symbol[fxn]]#", true);
// bad
tracery.testParseTag("stuff[action]symbol.mod1.mod2[postAction]", true);
tracery.testParseTag("[action]symbol.mod1.mod2[postAction]stuff", true);
tracery.testParse("#hero# ate some #color# #animal.s#", true);
tracery.testParse("#[#setPronouns#][#setOccupation#][hero:#name#]story#", true);
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment