User:Phlsph7/Readability.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Phlsph7/Readability. |
/* Userscript to highlight sentences by readability */
// Set score colors for css if they have not been defined
var readabilityScoreColors = readabilityScoreColors || [
'rgb(255,96,96)',
'rgb(255,128,128)',
'rgb(255,192,128)',
'rgb(255,224,128)',
'rgb(255,255,128)',
'rgb(214,255,128)',
'rgb(171,255,128)',
'rgb(128,255,128)',
'rgb(128,255,171)',
'rgb(128,255,214)',];
// Goes through all the p-elements and splits their content into span-elements. Each span corresponds to a sentence.
function divideParagraphsIntoSentences(){
let paragraphs = document.querySelectorAll('.mw-parser-output > p');
// Periods are the main guide for where sentences start and end.
// However, not all periods mark sentences, like in different forms of abbreviations.
// Placeholders are used for exceptions.
let periodPlaceholder = 'PERIOD_PLACEHOLDER';
let exceptionString = '...; Mr.; Mrs.; Dr.; Jr.; Sr.; Prof.; St.; Ave.; Corp.; Inc.; Ltd.; Co.; Gov.; Capt.; Sgt.; et al.; vs.; e.t.a.; .A.; .B.; .C.; .D.; .E.; .F.; .G.; .H.; .I.; .J.; .K.; .L.; .M.; .N.; .O.; .P.; .Q.; .R.; .S.; .T.; .U.; .V.; .W.; .X.; .Y.; .Z.; A.; B.; C.; D.; E.; F.; G.; H.; I.; J.; K.; L.; M.; N.; O.; P.; Q.; R.; S.; T.; U.; V.; W.; X.; Y.; Z.; .a.; .b.; .c.; .d.; .e.; .f.; .g.; .h.; .i.; .j.; .k.; .l.; .m.; .n.; .o.; .p.; .q.; .r.; .s.; .t.; .u.; .v.; .w.; .x.; .y.; .z.; .a; .b; .c; .d; .e; .f; .g; .h; .i; .j; .k; .l; .m; .n; .o; .p; .q; .r; .s; .t; .u; .v; .w; .x; .y; .z; 0.0; 0.1; 0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6; 1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0; 3.1; 3.2; 3.3; 3.4; 3.5; 3.6; 3.7; 3.8; 3.9; 4.0; 4.1; 4.2; 4.3; 4.4; 4.5; 4.6; 4.7; 4.8; 4.9; 5.0; 5.1; 5.2; 5.3; 5.4; 5.5; 5.6; 5.7; 5.8; 5.9; 6.0; 6.1; 6.2; 6.3; 6.4; 6.5; 6.6; 6.7; 6.8; 6.9; 7.0; 7.1; 7.2; 7.3; 7.4; 7.5; 7.6; 7.7; 7.8; 7.9; 8.0; 8.1; 8.2; 8.3; 8.4; 8.5; 8.6; 8.7; 8.8; 8.9; 9.0; 9.1; 9.2; 9.3; 9.4; 9.5; 9.6; 9.7; 9.8; 9.9. .0; .1; .2; .3; .4; .5; .6; .7; .8; .9;';
let exceptionStringSeparator = '; ';
let periodExceptions = exceptionString.split(exceptionStringSeparator);
let periodExceptionPlaceholders = exceptionString.split('.').join(periodPlaceholder).split(exceptionStringSeparator);
for(let paragraph of paragraphs){
let textContent = paragraph.textContent.split('\r').join('').split('\n').join('').trim();
// exclude very short paragraphs
if(textContent.length > 20){
divideIntoSentences(paragraph, periodExceptions, periodExceptionPlaceholders);
}
}
// remove very short sentences
let sentenceElements = document.getElementsByClassName('sentence');
for(let sentenceElement of sentenceElements){
let sentenceText = getSentenceText(sentenceElement);
if(sentenceText.trim().length < 10){
sentenceElement.classList.remove('sentence');
}
}
// Split the content of a p-element into span-elements. Each span corresponds to a sentence.
function divideIntoSentences(paragraph, periodExceptions, periodExceptionPlaceholders){
// Loop through all the nodes inside the p-element.
// Span-open-tags and close-tags are placed through code.
let innerHTML = getSpanStartTag();
let currentChild = paragraph.firstChild;
while(currentChild){
// if it is a text node, modify it
if(currentChild.nodeType === Node.TEXT_NODE){
innerHTML += adjustTextNodes(currentChild.nodeValue, periodExceptions, periodExceptionPlaceholders);
}
// if its an element, add outerHTML
else if(currentChild.nodeType === Node.ELEMENT_NODE){
innerHTML += currentChild.outerHTML;
}
// otherwise add nodeValue
else{
innerHTML += currentChild.nodeValue;
}
currentChild = currentChild.nextSibling;
}
innerHTML += '</span>';
paragraph.innerHTML = innerHTML;
// utility function to get the code for the opening span tag
function getSpanStartTag(){
return `<span class="sentence">`;
}
// utility function to get the code for span tags in the middle (closing + opening)
function getSpanEndAndStart(punctuation){
return punctuation + '</span>' + getSpanStartTag();
}
// utility function to modify text nodes
// they contain the punctuation relevant for sentences
function adjustTextNodes(text, periodExceptions, periodExceptionPlaceholders){
// use placeholders to remove all periods that do not mark sentences
text = insertPlaceholders(text, periodExceptions, periodExceptionPlaceholders);
// split using the remaining punctuation
text = text.split('.').join(getSpanEndAndStart('.'))
.split('!').join(getSpanEndAndStart('!'))
.split('?').join(getSpanEndAndStart('?'));
// use placeholders to return all periods that do not mark sentences
text = removePlaceholders(text, periodExceptions, periodExceptionPlaceholders);
return text;
function insertPlaceholders(text, periodExceptions, periodExceptionPlaceholders){
let modifiedText = text;
for(let i = 0; i < periodExceptions.length; i++){
modifiedText = modifiedText.split(periodExceptions[i]).join(periodExceptionPlaceholders[i]);
}
return modifiedText;
}
function removePlaceholders(text, periodExceptions, periodExceptionPlaceholders){
let modifiedText = text;
for(let i = 0; i < periodExceptions.length; i++){
modifiedText = modifiedText.split(periodExceptionPlaceholders[i]).join(periodExceptions[i]);
}
return modifiedText;
}
}
}
}
// Function to rate the readability of sentences and give them their class accordingly.
function rateSentences(){
// class names for different scores
const scoreClasses = [
'score-10-0',
'score-20-10',
'score-30-20',
'score-40-30',
'score-50-40',
'score-60-50',
'score-70-60',
'score-80-70',
'score-90-80',
'score-100-90'];
// Loop through all sentences, add their score class and their title attribute.
let sentenceElements = document.body.getElementsByClassName('sentence');
for(let sentenceElement of sentenceElements){
let sentenceText = getSentenceText(sentenceElement);
let score = getSentenceScore(sentenceText);
if(!isNaN(score)){
sentenceElement.title = `Score: ${score.toFixed(2)}`;
sentenceElement.dataset.sentenceText = sentenceText;
sentenceElement.dataset.score = score;
sentenceElement.classList.add(getScoreClass(score, scoreClasses));
}
else{
sentenceElement.classList.remove('sentence');
}
}
// Add the style sheet to color the score classes.
addScoreStyleSheet(scoreClasses, readabilityScoreColors);
function getScoreClass(score, scoreClasses){
let index = Math.floor(score / 10);
if(index < 0){
index = 0;
}
if(index > 9){
index = 9;
}
return scoreClasses[index];
}
function addScoreStyleSheet(scoreClasses, readabilityScoreColors){
const style = document.createElement('style');
for(let i = 0; i < scoreClasses.length; i++){
style.innerHTML += `.${scoreClasses[i]} {background-color: ${readabilityScoreColors[i]}; } `;
}
document.head.appendChild(style);
}
}
// Creates an overview at the top of the page
// This overview shows the readability of the whole article and other information
function createOverview(){
// Readability depends on the number of syllables, words, and sentences
let totalSyllableCount = 0;
let totalPolySyllableCount = 0;
let totalWordCount = 0;
const sentenceElements = document.getElementsByClassName('sentence');
let totalSentenceCount = sentenceElements.length;
const sentenceLengthArray = [];
for(let sentenceElement of sentenceElements){
let sentenceText = getSentenceText(sentenceElement);
let words = getWords(sentenceText);
totalWordCount += words.length;
sentenceLengthArray.push(words.length);
for(let word of words){
let syllableCount = getSyllableCount(word);
totalSyllableCount += syllableCount;
if(syllableCount >= 3){
totalPolySyllableCount++;
}
}
}
let totalReadability = getFleschKincaidReadability(totalSyllableCount, totalWordCount, totalSentenceCount);
let totalGradeLevel = getFleschKincaidGradeLevel(totalSyllableCount, totalWordCount, totalSentenceCount);
let totalSmogGradeLevel = getSmogGradeLevel(totalPolySyllableCount, totalSentenceCount);
const mainDiv = document.getElementById('mw-content-text');
const overviewDiv = document.createElement('div');
mainDiv.insertBefore(overviewDiv, mainDiv.firstChild);
const headline = document.createElement('h2');
overviewDiv.appendChild(headline);
headline.innerHTML = 'Readability overview';
const tableDiv = document.createElement('div');
overviewDiv.appendChild(tableDiv);
tableDiv.style.display = 'flex';
insertOverviewTable(tableDiv);
insertKeyTable(tableDiv);
//overviewDiv.appendChild(document.createElement('br'));
insertSentenceTable(overviewDiv);
function insertOverviewTable(parent){
const overviewTable = document.createElement('table');
parent.appendChild(overviewTable);
overviewTable.classList.add('wikitable');
overviewTable.style.marginRight = '20px';
/*const overviewCaption = document.createElement('caption');
overviewTable.appendChild(overviewCaption);
overviewCaption.innerHTML = 'Readability overview';*/
const overviewTableBody = document.createElement('tbody');
overviewTable.appendChild(overviewTableBody);
addRow(overviewTableBody, 'Readability (<a href="https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests">Flesch</a>)', totalReadability.toFixed(2));
addRow(overviewTableBody, 'Grade level (<a href="https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests">Flesch</a>)', totalGradeLevel.toFixed(2));
addRow(overviewTableBody, 'Grade level (<a href="https://en.wikipedia.org/wiki/SMOG">SMOG</a>)', totalSmogGradeLevel.toFixed(2));
addRow(overviewTableBody, 'Sentences', totalSentenceCount);
addRow(overviewTableBody, 'Words', totalWordCount);
addRow(overviewTableBody, 'Syllables', totalSyllableCount);
addRow(overviewTableBody, 'Average sentence length', (totalWordCount/totalSentenceCount).toFixed(2));
addRow(overviewTableBody, 'Standard deviation<br>of sentence length', (getStandardDeviation(sentenceLengthArray)).toFixed(2));
function getStandardDeviation(numbers){
const sum = numbers.reduce(function(sum, number){return sum + number;});
const mean = sum / numbers.length;
const varianceSum = numbers.reduce(function(sum, number){console.log(Math.pow(number - mean, 2)); return sum + Math.pow(number - mean, 2);}, 0);
const variance = varianceSum/numbers.length;
const standardDeviation = Math.sqrt(variance);
return standardDeviation;
}
}
// Key for the coloring
function insertKeyTable(parent){
const keyTable = document.createElement('table');
parent.appendChild(keyTable);
keyTable.classList.add('wikitable');
keyTable.innerHTML = `<thead>
<tr>
<th>Score</th>
<th>School level</th>
</tr>
</thead>
<tbody>
<tr class="score-100-90">
<td>100–90</td>
<td>5th grade</td>
</tr>
<tr class="score-90-80">
<td>90–80</td>
<td>6th grade</td>
</tr>
<tr class="score-80-70">
<td>80–70</td>
<td>7th grade</td>
</tr>
<tr class="score-70-60">
<td>70–60</td>
<td>8th & 9th grade</td>
</tr>
<tr class="score-60-50">
<td>60–50</td>
<td>10th to 12th grade</td>
</tr>
<tr class="score-50-40">
<td>50–40</td>
<td>College</td>
</tr>
<tr class="score-40-30">
<td>40–30</td>
<td>College</td>
</tr>
<tr class="score-30-20">
<td>30–20</td>
<td>College graduate</td>
</tr>
<tr class="score-20-10">
<td>20–10</td>
<td>College graduate</td>
</tr>
<tr class="score-10-0">
<td>10–0</td>
<td>College graduate</td>
</tr>
</tbody>`;
}
// sentence table to display all sentences ordered by lowest score
function insertSentenceTable(parent){
// button to show/hide the table
const sentenceTableButton = document.createElement('button');
parent.append(sentenceTableButton);
sentenceTableButton.innerHTML = 'Show sentences ordered by lowest score';
sentenceTableButton.style.fontSize = "24px";
// the table itself
const sentenceTable = document.createElement('table');
parent.appendChild(sentenceTable);
sentenceTable.classList.add('wikitable');
sentenceTable.style.display = 'none';
const sentenceCaption = document.createElement('caption');
sentenceTable.appendChild(sentenceCaption);
sentenceCaption.innerHTML = 'Sentences ordered by lowest score';
const sentenceTableBody = document.createElement('tbody');
sentenceTable.appendChild(sentenceTableBody);
// matrix to store the table values
const sentenceMatrix = [];
for(let sentenceElement of sentenceElements){
let sentenceText = sentenceElement.dataset.sentenceText;
let score = parseFloat(sentenceElement.dataset.score);
sentenceMatrix.push([sentenceText, score]);
}
// sort by lowest score
sentenceMatrix.sort(function(a, b){
return a[1] - b[1];
});
// loop through the matrix and add one row per index
for(let i = 0; i < sentenceMatrix.length; i++){
let sentenceText = sentenceMatrix[i][0];
let score = sentenceMatrix[i][1];
addRow(sentenceTableBody, sentenceText, score.toFixed(2));
}
// show/hide function of the button
sentenceTableButton.onclick = function(){
if(sentenceTableButton.innerHTML.includes('Show')){
sentenceTable.style.display = '';
sentenceTableButton.innerHTML = sentenceTableButton.innerHTML.split('Show').join('Hide');
}
else{
sentenceTable.style.display = 'none';
sentenceTableButton.innerHTML = sentenceTableButton.innerHTML.split('Hide').join('Show');
}
};
}
// utility function to add rows to a table
function addRow(tableBody, name, value){
let row = document.createElement('tr');
let nameCell = document.createElement('td');
nameCell.innerHTML = name;
row.appendChild(nameCell);
let valueCell = document.createElement('td');
valueCell.innerHTML = value;
row.appendChild(valueCell);
tableBody.appendChild(row);
}
}
// Utility function to extract the text from a sentence element
function getSentenceText(sentenceElement){
// hide references and certain templates
let refs = sentenceElement.querySelectorAll('.reference, .Inline-Template');
for(let ref of refs){
ref.style.display = 'none';
}
// the innerText attribute ignores hidden elements
let sentenceText = sentenceElement.innerText;
// show them again
for(let ref of refs){
ref.style.display = '';
}
// formatting
if(sentenceText[0] == '"'){
sentenceText = sentenceText.substring(1);
}
sentenceText = sentenceText.trim();
return sentenceText;
}
// utility function to get the readability score of a sentence
function getSentenceScore(sentenceText){
let syllableCount = 0;
let words = getWords(sentenceText);
for(let word of words){
syllableCount += getSyllableCount(word);
}
let wordCount = words.length;
let score = getFleschKincaidReadability(syllableCount, wordCount, 1);
return score;
}
// utility function: this is the main metric
function getFleschKincaidReadability(syllableCount, wordCount, sentenceCount){
let wordsPerSentence = wordCount / sentenceCount;
let syllablesPerWord = syllableCount / wordCount;
let score = 206.835 - (1.015 * wordsPerSentence) - (84.6 * syllablesPerWord);
return score;
}
// utility function: this shows the grade level and is used for the overview
function getFleschKincaidGradeLevel(syllableCount, wordCount, sentenceCount){
let wordsPerSentence = wordCount / sentenceCount;
let syllablesPerWord = syllableCount / wordCount;
let score = (0.39 * wordsPerSentence) + (11.8 * syllablesPerWord) - 15.59;
return score;
}
// utility function: get SMOG readability for the overview
function getSmogGradeLevel(polySyllableCount, sentenceCount){
let smogGradeLevel = 1.0430 * Math.sqrt(polySyllableCount * 30 / sentenceCount) + 3.1291;
return smogGradeLevel;
}
// utility function to extract words from a sentence
function getWords(sentenceText){
let punctuation = '.?!,;:"()[]{}--./&*#$%@+-=<>|~^\\' + "'";
for(let character of punctuation){
sentenceText = sentenceText.split(character).join('');
}
sentenceText = sentenceText.trim();
sentenceText = fullReplace(sentenceText, ' ', ' ');
let words = sentenceText.split(' ');
words = words.filter(function(word) {
return word.length > 0;
});
return words;
}
// utility function to count the syllables of a word
function getSyllableCount(word){
word = word.toLowerCase();
if (word.endsWith('e')) {
word = word.slice(0, -1);
}
word = word.split('e').join('a')
.split('i').join('a')
.split('o').join('a')
.split('u').join('a')
.split('y').join('a')
.split('e').join('a');
word = fullReplace(word, 'aa', 'a');
let syllableCount = word.split('a').length - 1;
if(syllableCount < 1){
syllableCount = 1;
}
return syllableCount;
}
// utility function to iteratively replace a string until no more occurrences are found
function fullReplace(string, oldSubstring, newSubstring){
let newString = string;
while(newString.includes(oldSubstring)){
newString = newString.split(oldSubstring).join(newSubstring);
}
return newString;
}
// anonymous main function
(function(){
// restrict script to mainspace, userspace, wikipedia, help, and draftspace
const namespaceNumber = mw.config.get('wgNamespaceNumber');
const allowedNamespaces = [0, 2, 4, 12, 118];
if (allowedNamespaces.indexOf(namespaceNumber) != -1) {
// add a link to the toolbox
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function (){
var portletlink = mw.util.addPortletLink('p-tb', '#', 'Readability');
// run the main function when the link is clicked
portletlink.onclick = function(e) {
e.preventDefault();
divideParagraphsIntoSentences();
rateSentences();
createOverview();
};
});
}
})();