Home For Fiction – Blog

for thinking people


January 15, 2024

An Iambic Pentameter Checker in JavaScript

Programming

iambic, javascript, literature, programming

My iambic pentameter generator has been one of the most popular posts of the blog. To be honest, I’m not sure if that’s still the case or not. Since I completely revamped the blog (actually even earlier), I removed Google and Jetpack analytics. In any case, today we’ll be looking at, essentially, the reverse scenario: We’re making a very simple iambic pentameter checker.

Just in case you need a reminder, an iambic pentameter line consists of ten syllables, of which every other is stressed. For instance, “And you, my sinews, grow not instant old” (from Hamlet).

So, how can we create a JavaScript iambic pentameter checker that tells us if a line is an iambic pentameter or not?

iambic pentameter checker, AI render of Shakespeare using a computer
I couldn’t resist using AI to generate this image of Shakespeare using a laptop. Like all tools, AI tools can be fun and useful, if one understands their limitations…

An Iambic Pentameter Checker: The Basic Setup

To make an iambic pentameter checker we need to check two things:

Unless you are a programming god (and I’m certainly not), someone has already thought of what you’ve tried to put together. In other words, there’s absolutely no need to reinvent the wheel. I decided to use the excellent Datamuse API for our purposes, since it returns the number of syllables as well as the stress patterns.

The Easy and the not-so-Easy

I will share with you the code in a moment, so you can see for yourself, but here’s the brief outline of my plan:

Everything up and including counting the syllables was very simple. Problems began with the stress pattern detection, for a simple reason: Monosyllabic words.

You see, the words “And you, my” display a pattern of 0 1 0 in Shakespeare’s play – indeed, in everyday speech, too; it sounds unnatural to stress them like e.g. “AND you” . However, Datamuse treats them as stressed and returns 1 1 1 . This was highly problematic.

In the end, I decided to treat monosyllabic words as special cases, effectively allowing them to be either stressed or unstressed.

Iambic Pentameter Checker: The Program

Usual caveats: The program runs in an iframe which links to raw.githack. This means that, as a free service, 100% uptime cannot be guaranteed. Moreover, if Datamuse is down or slow, the program won’t work either.

Click to run the program

Try it with “And you, my sinews, grow not instant old” – avoid exotic punctuation, unnecessary blank spaces, etc. I’ve included some input validation, but this is a lazy-morning project. If you absolutely want to break the code, you’ll find a way of doing it, I’m sure…

You can try up to 10 lines at a time, separated by a line break (i.e. press “Enter” at the end of each line).

Just to repeat the notes also displaying above: This assessment is not exact science. Computer-based detections of emphasis patterns are problematic, and actual pronunciations might differ for artistic reasons.

The Code of this Iambic Pentameter Checker

You can find the code on my GitHub page. Or, if you’re bored to go there, here it is:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta name="robots" content="noindex">
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
		integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
		crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<style>
    body {
        background-color: black;
        color: gainsboro;
        font-family: Arial, sans-serif;
        text-align: center;
        margin: 0;
        padding: 0;
    }

    input[type="text"] {
		padding: 10px;
		font-size: 16px;
		border-radius:0.5em;
    }

    button {
        padding: 10px 20px;
		border-radius:0.5em;
		margin-top:1em;
        font-size: 16px;
        background-color: #555;
        color: white;
        border: none;
        cursor: pointer;
    }
	button:hover {animation: clickEffect 0.5s ease;animation-fill-mode: forwards;}
    @keyframes clickEffect {
        100% {
            background-color: #333;
        }
    }
	button:focus {animation: clickEffect2 0.5s ease;}
	@keyframes clickEffect2 {
	    0% {
            background-color: #555;
        }
        50% {
            background-color: green;
        }
        100% {
            background-color: #555;
        }
	}
	#again, #outcome2 {
		display:none;
		margin-bottom:1em;
	}
</style>
<body>
	<textarea id="inputText" rows="4" cols="50" placeholder="Type here…"></textarea>
    <br>
    <button id="submit">Submit</button>
	<button id="again">Clear/Reload</button>
	<div id="outcome"></div>
	<div id="outcome2">
		<p>An iambic pentameter requires precisely 10 syllables and an emphasis pattern of '0 1 0 1 0 1 0 1 0 1', that is, every other syllable is emphasized. Due to the nature of monosyllabic words within a phrase (as well as limitations of the emphasis detection library), monosyllabic words are marked with <span style='font-size:1.1em'>X</span> and are treated as either 0 or 1.</p>
		<p>In any case, keep in mind that this assessment <em>is not</em> exact science. Computer-based detections of emphasis patterns are problematic, and actual pronunciations might differ for artistic reasons.</p>
	</div>
</body>
<script>
let linesArray = []; 
document.getElementById("inputText").value = "";

$("#again").click(function(){
	window.location.reload()
})

$("#submit").click(function(){
	linesArray = document.getElementById("inputText").value.split("\n");
	$("#submit").blur();
	if (linesArray.length==1 && linesArray[0]=="") {
		alert("You didn't type anything…");
		return;
	}
	if (linesArray.length > 10) {
		alert("Enter up to 10 lines…");
		return;
	}
	
	for (let i=0;i<linesArray.length;i++) {
		let contentArray = linesArray[i].trim().replace(/[^\w\s]/g, '').toLowerCase().split(' ').filter(Boolean);
		if (contentArray.length<11) {
			processLine(contentArray, i, false);
		}
		else {
			processLine(contentArray, i, true);
			//alert("You have entered a line with more than 10 syllables. By definition, it can't be an iambic pentameter…");
		}	
	}
	
	$("#submit").fadeOut(200, function(){
		$("#again").fadeIn(500);
		$("#outcome2").fadeIn(500);
	});

});

function processLine(arr, lineNo = 0, isOver10Syl = false) {
	let processEnded = false;
	if (linesArray.length == lineNo+1) {
		processEnded = true;
	}
	let syllablesSum;
	if (isOver10Syl) {
		arr = [];
	}
	const promises = arr.map(word => getSyllables(word));
	Promise.all(promises)
		.then(results => {
			syllablesSum = results.map(obj => obj[0].numSyllables);
			
			const pronPatterns = arr.map(word => getEmph(word));
			return Promise.all(pronPatterns);
		})
		.then(res => {
			let emphPatterns = res.map(obj => obj[0].tags[0]);
               let lineData = { syllablesSum, emphPatterns }; 
			processResults(lineData, lineNo, processEnded);
		})
		.catch(error => {
			console.error("Error:", error);
		});
}

function getSyllables(word) {
    return new Promise(function(resolve, reject) {
        $.get('https://api.datamuse.com/words?sp=' + word + '&max=1&md=s')
            .done(data => resolve(data))
            .fail(error => reject(error));
    });
}

function getEmph(word) {
    return new Promise(function(resolve, reject) {
        $.get('https://api.datamuse.com/words?sp=' + word + '&max=1&md=r')
            .done(data => resolve(data))
            .fail(error => reject(error));
    });
}

function processResults(lineData, lineNo, processEnded) {
    let syllablesSum = lineData.syllablesSum;
    let emphPatterns = lineData.emphPatterns;
	let theLineNumber = parseInt(lineNo)+1;
	let theOutcome, outcomeStr;
	//calculate syllable number;
	let cnt = 0;
	syllablesSum.forEach(function(el){
		cnt = cnt+el;
	});
	if (cnt == 0) {
		cnt = ">10";
	}

    // calculate emphasis pattern
    let tmpArr = [];
    emphPatterns.forEach(function (el, index) {
        if (syllablesSum[index] === 1) {
            tmpArr.push("X");
			emphPatterns[index] = "X";
        } else {
            tmpArr.push(getEmphasisFromPattern(el));
        }
    });
    tmpArr = tmpArr.flat();
	
	if (checkIamb(tmpArr) && cnt===10) {
		theOutcome = true;
		outcomeStr = "<span style='color:lightgreen'>This is an iambic pentameter.</span>";
	}
	else {
		theOutcome = false;
		outcomeStr = "<span style='color:crimson'>This is not an iambic pentameter.</span>";
	}
	
	//displaying results
	let encDiv = document.createElement("div");
	encDiv.id = "theDiv"+theLineNumber;
	let str0 = "Line #"+theLineNumber;
	let str1 = "<p>Number of syllables: <span style='font-size:1.1em'>" + cnt + "</span></p>";
	let str2 = "<p>Emphasis pattern: <span style='font-size:1.1em'>" + tmpArr.join(" ") + "</span></p>";
	let str3 = "<p>Assessment: " + outcomeStr + "</p><br>";
	$(encDiv).html(str0+str1+str2+str3);
	document.getElementById("outcome").appendChild(encDiv);

	if (processEnded) { // arrange line results 
		const outcomeDiv = document.getElementById("outcome");
		const children = Array.from(outcomeDiv.children);

		children.sort((a, b) => {
		  const numA = parseInt(a.id.replace("theDiv", ""), 10);
		  const numB = parseInt(b.id.replace("theDiv", ""), 10);
		  return numA - numB;
		});

		outcomeDiv.innerHTML = ""; // Clear existing content
		children.forEach(child => outcomeDiv.appendChild(child));
	}

}

function checkIamb(tmpArr) {
    const expectedPattern = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1];
    for (let i = 0; i < tmpArr.length; i++) {
        // If tmpArr[i] is "X," skip the comparison
        if (tmpArr[i] !== "X") {
            if (tmpArr[i] !== expectedPattern[i]) {
                return false;
            }
        }
    }

    return true;
}


function getEmphasisFromPattern(pattern) {
    const numericParts = pattern.match(/\d+/g);
    const emphasisArray = numericParts ? numericParts.map(part => parseInt(part)) : [];
    return emphasisArray;
}

</script>
</html>

Feel free to play with it and improve it further!