NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name (OLD)User Glossary for MTL
// @version 1.3.5
// @license MIT
// @namespace lnmtl_glossary
// @description User glossary fetched from google docs spreadsheet for lnmtl.com
// @author Kronos
// @match http*://lnmtl.com/*
// @require https://code.jquery.com/jquery-2.2.4.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.5.2/tabletop.min.js
// @require https://userscripts-mirror.org/scripts/source/107941.user.js
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
const storageCleanupInterval = 3600000 * 30;
const glossaryChunkSize = 256;
const novelRefreshTime = 6;
let publicSpreadsheetUrl = GM_SuperValue.get("publicSpreadsheetUrl", "");
let replaceInOriginal = GM_SuperValue.get("repalceInOriginal", false);
let noWordWrap = GM_SuperValue.get("noWordWrap", false);
let reduceContrast = GM_SuperValue.get("reduceContrast", false);
let useLNMTLGlossary = GM_SuperValue.get("useLNMTLGlossary", false);
let applyLNMTLGlossary = GM_SuperValue.get("applyLNMTLGlossary", false);
let autoSolveConflicts = GM_SuperValue.get("autoSolveConflicts", false);
let nextStorageCleanup = GM_SuperValue.get("nextStorageCleanup", 0);
let disableGlossaryAnalyzer = GM_SuperValue.get("disableGlossaryAnalyzer", false);
let rawsReplaced = false;
let reload = false;
let LNMTLGlossaryLock = Promise.resolve();
const regexGlossaryTerms = /(\d+)\) (?:(\S+) )?=>(?: (.+))?$/;
let myTime = new Date().getTime();
if (myTime >= nextStorageCleanup || nextStorageCleanup - myTime >= storageCleanupInterval) {
setTimeout(function() {
nextStorageCleanup = myTime;
Object.keys(localStorage).forEach(key => {
if (key.startsWith("ndata")) {
let data = JSON.parse(localStorage[key]);
if (myTime - data.time >= storageCleanupInterval) {
localStorage.removeItem("gdata" + data.novel_id);
localStorage.removeItem("gvalid" + data.novel_id);
sessionStorage.removeItem("gmdata" + data.novel_id);
localStorage.removeItem(key);
}
else if (nextStorageCleanup > data.time) { nextStorageCleanup = data.time; }
}});
GM_SuperValue.set("nextStorageCleanup", nextStorageCleanup += storageCleanupInterval);
}, 30000);
}
function store(name, value){
if (typeof(Storage) !== "undefined") {
try {
return localStorage.setItem(name, JSON.stringify(value));
} catch(e) {
// Cleanup on all errors (full: e.code == 22)
Object.keys(localStorage).forEach(key => { if (key.startsWith("ndata") || key.startsWith("gdata") || key.startsWith("gvalid")){ localStorage.removeItem(key);}});
Object.keys(sessionStorage).forEach(key => { if (key.startsWith("gmdata")) { sessionStorage.removeItem(key);}});
}
} else {
GM_SuperValue.set(name, value);
}
}
function storeSession(name, value){
if (typeof(Storage) !== "undefined") {
try {
return sessionStorage.setItem(name, JSON.stringify(value));
} catch(e) {
// Cleanup on all errors (full: e.code == 22)
Object.keys(sessionStorage).forEach(key => { if (key.startsWith("gmdata")) { sessionStorage.removeItem(key);}});
}
}
}
function restoreSession(name, defaultValue){
if (typeof(Storage) !== "undefined") {
let item = sessionStorage.getItem(name);
if(item === null && defaultValue !== undefined)
return defaultValue;
return JSON.parse(item);
}
return defaultValue;
}
function removeSession(name){
if (typeof(Storage) !== "undefined") {
return sessionStorage.removeItem(name);
}
}
function restore(name, defaultValue){
if (typeof(Storage) !== "undefined") {
let item = localStorage.getItem(name);
if(item === null && defaultValue !== undefined)
return defaultValue;
return JSON.parse(item);
} else {
// No Web Storage support..
GM_SuperValue.get(name, defaultValue);
}
}
function removeStorage(name){
if (typeof(Storage) !== "undefined") {
return localStorage.removeItem(name);
}
}
String.prototype.count = function (regexp) {
if (typeof regexp === 'string' || regexp instanceof String)
regexp = new RegExp(regexp.escapeRegExp(), 'g');
let res = this.match(regexp);
return res !== null ? res.length: 0;
};
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function lock(func) {
return new Promise(resolve => { func().then(() => resolve()); });
}
function fetchLNMTLNovelData(url, callback) {
return $.get(url, function(response) { return callback(response); });
}
function loadLNMTLGlossary(url, callback, withMetaData) {
if ($("#navbar a:contains('Login')").length > 0){
return Promise.resolve(); // not logged in
}
withMetaData = withMetaData || false;
if (!useLNMTLGlossary)
callback = callback || function() { return Promise.resolve();};
else
callback = callback || applyGlossaryAsync;
let novelData = restore("ndata" + url);
let timeNow = new Date().getTime();
if (novelData === null )
novelData = { time: 0 , changes: 0 };
let old_changes = novelData.changes;
let old_retrans = novelData.retrans;
let callGetLNMTLGLossary = function (novelData, old_changes, old_retrans, callback, withMetaData){
return getLNMTLGlossary(novelData.novel_id, novelData.changes, ((novelData.changes - old_changes <= 0) && old_retrans === novelData.retrans), callback, withMetaData);
};
if (timeNow - novelData.time >= 3600000 * novelRefreshTime){
console.log("fetch novel data: " + url);
return new Promise( resolve => {
fetchLNMTLNovelData(url, function (data) {
let novelinfo = $('.panel.panel-default .progress-bar[role="progressbar"]', $(data));
let glossary_changes = novelinfo.text().trim().split(' / ');
let retranslation_time = new Date($(".panel.panel-default dt:contains('Latest retranslation at') ~ * span", $(data)).text()).getTime();
let novel_id = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']", $(data)).attr("href").split("https://lnmtl.com/termProposition?novel_id=")[1];
novelData.time = timeNow;
novelData.changes = glossary_changes[0];
novelData.retrans = retranslation_time;
novelData.novel_id = novel_id;
store("ndata" + url, novelData);
callGetLNMTLGLossary(novelData, old_changes, old_retrans, callback, withMetaData).then((value) => resolve(value));
}).fail(e => resolve());
});
} else {
return callGetLNMTLGLossary(novelData, old_changes, old_retrans, callback, withMetaData);
}
}
function analyseGlossary(dict, metaDict, monitor){
return new Promise(function(resolve, reject) {
monitor = monitor || function() {};
metaDict = metaDict || [];
let reorder = {};
let badorder = {};
let goodorder = {};
let duplicates = {};
let whitespaces = [];
let chunk = 100;
let pcount = 0;
let i = 0;
function processTerms() {
let cnt = chunk;
for (;i < dict.length && ( cnt-- > 0); i++) {
let term = dict[i];
if (term.raw == "") //#skip empty raw
continue;
let j = i + 1;
let after_term_pos = i;
while (j < dict.length){
if (i === j){
break;}
let bterm = dict[j];
//filter dublicate entries
if (term.raw == bterm.raw){
if (j > i)
duplicates[i] = [j];
}
else {
if (bterm.raw.includes(term.raw) && after_term_pos < j) {
if(j > i){
reorder[i] = j;
after_term_pos = j;
(badorder[i] || (badorder[i] = [])).push(j);
}
else{
(goodorder[i] || (goodorder[i] = [])).push(j);
}
}
}
if (++j == dict.length && after_term_pos != i){
after_term_pos = -1;
j = 0;
}
}
if (term.meaning.trim() != term.meaning)
whitespaces.push(term); // whitespaces removed during parsing!
}
if (pcount % 10 == 0)
monitor(i, dict.length);
if (i < dict.length) {
pcount++;
setTimeout(processTerms, 1);
}
else{
monitor(100, 100); // progress complete
let reverseDict = function(map, f) {
let id = function(x) {return x;};
return Object.keys(map).reduce(function(acc, k) {
acc[map[k]] = (acc[map[k]] || []).concat((f || id)(k));
return acc;
},{});
};
duplicates = reverseDict(duplicates);
let conflicts = reverseDict(reorder);
for(let key in conflicts){
if(!goodorder[key])
goodorder[key] = conflicts[key];
}
let metadata = {"terms": dict.length,"conflicts": Object.keys(reorder).length, "duplicates": Object.keys(duplicates).length, "whitespaces": whitespaces.length};
let realPosition = (pos) => (metaDict[pos]||[parseInt(pos)+1])[0];
let posWrapper = (term, pos) => { return {"raw":term.raw.replace(/\\\\/g,''), "meaning":term.meaning, "pos":realPosition(pos)}; };
let infoWrapper = (term, pos) => { return {"raw":term.raw.replace(/\\\\/g,''), "meaning":term.meaning, "pos":realPosition(pos), "goodorder":termInfos(goodorder[pos], posWrapper), "badorder": termInfos(badorder[pos], posWrapper)}; };
let termInfos = (terms, wrapper) => (terms||[]).reduce((p, i) => p.concat(wrapper(dict[i],i)), []);
let conflictResult = Object.keys(conflicts).reduce((p, key) => {
return function(p, term, key, value) {
return p.concat([termInfos(value, infoWrapper).concat([infoWrapper(term, key)])]);
}(p, dict[key], key, conflicts[key]);
}, []);
let duplicatesResult = Object.keys(duplicates).reduce((p, key) => {
return function(p, term, key, value) {
return p.concat([termInfos(value, posWrapper).concat([posWrapper(term, key)])]);
}(p, dict[key], key, duplicates[key]);
}, []);
let result = {"metadata":metadata, "conflicts":conflictResult, "duplicates": duplicatesResult, "whitespaces":whitespaces};
resolve(result);
}
}
processTerms();
});
}
function processArrayAsync(arr, fn, maxTimePerChunk, chunk, context) {
context = context || window;
maxTimePerChunk = maxTimePerChunk || 200;
chunk = chunk || 50;
let i = 0;
function now() {
return new Date().getTime();
}
function processChunk() {
let startTime = now();
let cnt = chunk;
while (i < arr.length && ( cnt-- > 0 || (now() - startTime) <= maxTimePerChunk)) {
if (cnt <= 0){
cnt = chunk;
}
// callback called with (value, index, array)
fn.call(context, arr[i], i++, arr);
}
if (i < arr.length) {
setTimeout(processChunk, 1);
}
}
processChunk();
}
function getLNMTLGlossary(novelid, changeCount, skipUpdate, callback, withMetaData){
withMetaData = withMetaData || false;
let glossaryData = (restore("gdata" + novelid));
let glossaryMetaData;
if(withMetaData && !restore("gvalid" + novelid, false)){
removeSession("gmdata"+ novelid);
}
if (glossaryData === null || (withMetaData && ((glossaryMetaData = restoreSession("gmdata" + novelid)) === null))){
glossaryData = [];
glossaryMetaData = [];
skipUpdate = false;
}
if (!skipUpdate){
console.log("fetch novel glossary:" + novelid);
return new Promise( resolve => {
fetchLNMTLGlossary(novelid, function (data){
let o = $('#after_term_id', $(data))[0].options;
processArrayAsync(o, (v,i) => {
let g = (regexGlossaryTerms.exec(v.text) || []);
let dict = {};
dict.raw = (g[2]||"").trim().replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\\\$&");
dict.meaning = (g[3]||"").trim();
//dict.id = v.value;
glossaryData[i] = dict;
if(withMetaData)
glossaryMetaData[i] = [parseInt(g[1]),parseInt(v.value)];
});
//glossaryData = $.map($('#after_term_id option', $(data)).get(), function( a ) { let dict = {}; return dict;});
store("gdata" + novelid, glossaryData);
if(withMetaData){
storeSession("gmdata" + novelid, glossaryMetaData);
store("gvalid" + novelid, true); //indicate if sessionStorage and localStorage are consistent
} else if (restore("gvalid" + novelid, false))
removeStorage("gvalid" + novelid); //invalidate sessionStorage on localStorage change
//console.log("dict length ="+ glossaryData.length);
callback(glossaryData, changeCount, glossaryMetaData, novelid).then((value) => resolve(value));
}, resolve);
});
}
else {
//console.log("store dict length ="+ glossaryData.length);
return callback(glossaryData, changeCount, glossaryMetaData, novelid);
}
}
function fetchLNMTLGlossary(novelid, callback, resolve, term_id, trys) {
if (!term_id)
term_id = 1;
if (!trys)
trys = 10;
$.get("https://lnmtl.com/termProposition/create?type=move&novel_id=" + novelid + "&term_id="+term_id, function(response) {
callback(response);
}).fail(function(e) { // workaround if id 1 is missing.
if (e.status == 404) {
if(trys > 0 || term_id < 4196)
return fetchLNMTLGlossary(novelid, callback, resolve, term_id*2, trys-1);
else
resolve();
} else {
resolve();
}
});
}
String.prototype.escapeRegExp = function () {
let regFilter = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
return this.replace(regFilter, "\\$&");
};
function escapeRegExp(str) {
let regFilter = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
if (str.constructor === Array){
let newStr = [];
for(let i in str){
newStr.push(str[i].replace(regFilter, "\\$&"));
}
return newStr;
}
return str.replace(regFilter, "\\$&");
}
function fetch_glossary() {
Tabletop.init( { key: publicSpreadsheetUrl,
callback: useData,
simpleSheet: false } );
}
function updateReplace(replace){
replaceInOriginal = replace;
GM_SuperValue.set("repalceInOriginal", replaceInOriginal);
}
function updateDocsURL(url){
if(!(url.startsWith("https://") || url.startsWith("http://") || url === ""))
// no url no save
return;
publicSpreadsheetUrl = url;
GM_SuperValue.set("publicSpreadsheetUrl", publicSpreadsheetUrl);
}
function updateUseLNMTLGlossary(_useLNMTLGlossary){
useLNMTLGlossary = _useLNMTLGlossary;
GM_SuperValue.set("useLNMTLGlossary", useLNMTLGlossary);
if (window.location.pathname.startsWith("/chapter/") || window.location.pathname.startsWith("chapter/") && useLNMTLGlossary) {
let novelname = window.location.pathname.split("/").pop();
novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
let book_index = novelname.lastIndexOf("-book-");
if(book_index > novelname.length - 10 && book_index > 0){
novelname = novelname.substr(0, book_index);
}
LNMTLGlossaryLock = lock(() => loadLNMTLGlossary("https://lnmtl.com/novel/" + novelname));
fetch_glossary(); //google docs glossary
}
}
function updateDisableGlossaryAnalyzer(_disableGlossaryAnalyzer){
disableGlossaryAnalyzer = _disableGlossaryAnalyzer;
GM_SuperValue.set("disableGlossaryAnalyzer", disableGlossaryAnalyzer);
if (window.location.pathname.startsWith("/novel/") || window.location.pathname.startsWith("novel/") && !disableGlossaryAnalyzer) {
if (!disableGlossaryAnalyzer)
if ($('#novel-display-conflicts-button').length == 0){
addGlossaryAnalyseModal();
addAnalyseButton();
}
else
$('#novel-display-conflicts-button').show();
else $('#novel-display-conflicts-button').hide();
}
}
function updateContrast(_reduceContrast){
reduceContrast = _reduceContrast;
GM_SuperValue.set("reduceContrast", reduceContrast);
changeContrast(reduceContrast);
}
function updateWordWrap(_noWordWrap){
noWordWrap = _noWordWrap;
GM_SuperValue.set("noWordWrap", noWordWrap);
changeWordWrap(noWordWrap);
}
function addCloseSettingsListener(){
$("#chapter-display-options-modal div.modal-footer button").click(function(){$('#chapter-display-options-modal div.modal-body #gdocs-url-input').val(publicSpreadsheetUrl);});
}
function addUserGlossaryLink(){
if(publicSpreadsheetUrl !== ""){
// if the gdocs url links to the web version than move to edit mode
let editurl = publicSpreadsheetUrl.replace("/pubhtml","/edit");
let ex_icon = '<sup> <span class="glyphicon glyphicon-new-window"></span></sup>';
let link = $('<li><a target="_blank" href="'+editurl+'">Google User Glossary'+ex_icon+'</a></li>');
$('#navbar > ul.nav.navbar-nav.navbar-right > li.dropdown .dropdown-header:contains("Glossary") ~ .divider').before(link);
}
}
function addGlossarySettings(){
let title = $("<h3>Advanced Glossary</h3>");
let row = $('<div class="row"></div>');
let col = $('<div class="col-xs-6"></div>');
let label = $('<label class="control-label">User Glossary Sheet Url:</label>');
let textarea_row =$('<div class="col-xs-12"></div>');
let textarea =$('<textarea class="form-control" id="gdocs-url-input" rows="2" input type="text" name="gdocs-url" wrap="soft">');
textarea.val(publicSpreadsheetUrl);
textarea.on("change keyup paste", function(){updateDocsURL(textarea.val());});
textarea_row.append(textarea);
col.append(label);
let col2 = $('<div class="col-xs-6"></div>');
let checked = "";
if(replaceInOriginal)
checked = "checked";
let option2 = $('<sub><input id="replaceInOriginal" type="checkbox" '+checked+'></sub> <label for="replaceInOriginal">Replace all raws in source sentence</label>');
option2.on("change", function(){updateReplace($("#replaceInOriginal")[0].checked);});
col2.append(option2);
row.append(col).append(col2).append(textarea_row);
$("#chapter-display-options-modal .modal-body").append(title).append(row);
if ($("#navbar a:contains('Login')").length > 0){
// not logged in
return;
}
let LNMTLGlossaryChecked = "";
if (useLNMTLGlossary)
LNMTLGlossaryChecked = "checked";
let col3 = $('<div class="col-xs-12"></div>');
let option3 = $('<sub><input id="useLNMTLGlossary" type="checkbox" '+LNMTLGlossaryChecked+'></sub> <label for="useLNMTLGlossary">Apply approved LNMTL Glossary entries to chapters. (cache refreshes every ' + novelRefreshTime+'h)</label>');
option3.on("change", function(){updateUseLNMTLGlossary($("#useLNMTLGlossary")[0].checked);});
let GlossaryAnalyzerChecked = "";
if (disableGlossaryAnalyzer)
GlossaryAnalyzerChecked = "checked";
let col4 = $('<div class="col-xs-12"></div>');
let option4 = $('<sub><input id="disableGlossaryAnalyzer" type="checkbox" '+GlossaryAnalyzerChecked+'></sub> <label for="disableGlossaryAnalyzer">Disable Glossary Analyser</label>');
option4.on("change", function(){updateDisableGlossaryAnalyzer($("#disableGlossaryAnalyzer")[0].checked);});
row.append(col3.append(option3)).append(col4.append(option4));
}
function createConflictReport(data, showPopover, caption){
showPopover = showPopover || false;
if (caption)
caption = '<caption align="bottom">' + caption + '</caption>';
else
caption = "";
let popdiv = $('<div id=popoverConflictTarget> </div>');
let table = $('<table class="table table-sm table-condensed"> '+ caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> <th scope="col">glossary position</th> </tr> </thead> </table>');
let tdata = '';
let delimiter = '<tr class="active"> <th scope="row" colspan="3"></th> </tr>';
let addPopover = function(text, popover) {
if(!showPopover)
return text;
return '<td tabindex="0" data-toggle="popover" data-trigger="focus" title="glossary order" data-delay="{ "show": 800, "hide": 400 }" data-placement="auto" data-content="'+popover+'" data-html="true" >' + text + '</td>';
};
let locationContext = function (term){
let result = "<ul class='list-group'>";
let good = term.goodorder;
let bad = term.badorder;
for(let key in good){
result +="<li class='list-group-item'>"+ termPosition(good[key])+"</li>";
}
result +="<li class='list-group-item active'>"+ termPosition(term)+"</li>";
for(let key in bad){
result += "<li class='list-group-item'>"+ termPosition(bad[key])+"</li>";
}
return result + '</ul>';
};
let termPosition = function (term){
return term.pos + " | " + term.raw;
};
for (i=0; i < data.length; i++ ) {
for (j=0; j < data[i].length; j++ ) {
let term = data[i][j];
tdata += '<tr> <th scope="row">' + term.raw + '</th> ' + addPopover(term.meaning,locationContext(term)) + '<td>'+ term.pos + '</td> </tr>';
}
tdata += delimiter;
}
let tbody = $('<tbody>' + tdata + '</tbody>');
table.append(tbody);
return popdiv.append(table);
}
function createTermGroupReport(data, caption){
if (caption)
caption = '<caption align="bottom">' + caption + '</caption>';
else
caption = "";
let table = $('<table class="table" table-sm table-condensed> ' + caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> <th scope="col">position</th> </tr> </thead>');
let tdata = '';
let delimiter = '<tr class="active"> <th scope="row" colspan="3"></th> </tr>';
for (i=0; i < data.length; i++ ) {
for (j=0; j < data[i].length; j++ ) {
let term = data[i][j];
tdata += '<tr> <th scope="row">' + term.raw + '</th> <td>' + term.meaning + '</td> <td>'+ term.pos + '</td> </tr>';
}
tdata += delimiter;
}
let tbody = $('<tbody>' + tdata + '</tbody>');
table.append(tbody);
return table;
}
function createTermReport(data,caption){
if (caption)
caption = '<caption align="bottom">' + caption + '</caption>';
else
caption = "";
let table = $('<table class="table table-sm table-condensed">' + caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> </tr> </thead>');
let tdata = '';
for (i=0; i < data.length; i++ ) {
let term = data[i];
tdata += '<tr> <th scope="row">' + term.raw + '</th> <td>' + term.meaning + '</td> </tr>';
}
let tbody = $('<tbody>' + tdata + '</tbody>');
table.append(tbody);
return table;
}
function createAnalyzerReport(data){
let intro = $('<p></p>');
let metadata = data.metadata;
let plural = 's';
let info1 = '<h5><span class="label label-default">'+metadata.conflicts+'</span> of <span class="label label-default">'+metadata.terms+'</span> terms out of order</h5>' ;
if (metadata.duplicates == 1)
plural = '';
let info2 = '<h5><span class="label label-default">'+metadata.duplicates+'</span> duplicate'+plural+' detected</h5>';
plural = 's';
if (metadata.whitespaces == 1)
plural = '';
let info3 = '<h5><span class="label label-default">'+ metadata.whitespaces+'</span> term'+plural+' with misplaced whitespaces</h5>';
let head = intro.append($(info1));
if(metadata.conflicts != 0){
let conflictReport = createConflictReport(data.conflicts, true, "conflicting groups of terms");
head.append(conflictReport);
}
if(metadata.duplicates != 0){
if(metadata.conflicts != 0)
head.append($('<hr>'));
let duplicateReport = createTermGroupReport(data.duplicates, "duplicate terms");
head.append(info2).append(duplicateReport);
}
if(metadata.whitespaces != 0){
if(metadata.conflicts != 0 || metadata.duplicates != 0)
head.append($('<hr>'));
let whitespaceReport = createTermReport(data.whitespaces, "terms with leading or trailing whitespace");
head.append(info3).append(whitespaceReport);
}
return head;
}
function addAnalyseButton(){
let firstBtn = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']:contains('Check New Term Propositions')" );
firstBtn.after($('<button type="button" id="novel-display-conflicts-button" class="btn btn-warning" data-toggle="modal" data-target="#novel-display-conflicts-modal"> Analyse Glossary</button>'));
}
function addGlossaryAnalyseModal() {
//anylyse glossary
// conflics, duplicates, whitespace terms
let novelname = $('.novel-name')[0].firstChild.textContent.trim();
let module = $('<div class="my-module"> <div class="modal fade" id="novel-display-conflicts-modal"> <div class="modal-dialog"> <div class="modal-content"> </div> </div> </div> </div>');
let modal_header = $('<div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> <h4 class="modal-title"> Analyse <em>' + novelname + '</em> Glossary</h4> </div>');
let modal_body = $('<div class="modal-body"></div>');
let progress_bar = $('<div class="collapse" id="collapseProgressbar"> <div class="progress"> <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" id="progress-bar-scan" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%; min-width: 3em;"> Scan Glossary </div> </div> </div>');
let modal_footer = $('<div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>');
modal_body.append(progress_bar);
$('.modal-content', module).append(modal_header).append(modal_body).append(modal_footer);
$( "#chapter-display-options-modal" ).after(module);
let pMonitor = (value, maxvalue) => progressMonitor($('#progress-bar-scan', module), value, maxvalue);
pMonitor(0,100);
$('#novel-display-conflicts-modal').on('shown.bs.modal', scan = function(e) {
$('#novel-display-conflicts-modal').off('shown.bs.modal', scan);
loadLNMTLGlossary(window.location.toString(), function (dict, changes, metaDict) { return Promise.resolve(analyseGlossary(dict, metaDict, pMonitor));}, true)
.then((results) => modal_body.append(createAnalyzerReport(results)))
.then(() => setTimeout(() => $('#collapseProgressbar').collapse('hide'),1000))
.then(() => $('#novel-display-conflicts-modal [data-toggle="popover"]').popover({container: "#popoverConflictTarget"}));
});
$('#novel-display-conflicts-modal').on('hidden.bs.modal', () => $('#novel-display-conflicts-button').focus(function() { this.blur();}));
$('#chapter-display-conflicts-modal > .modal').modal("show");
$('#collapseProgressbar').collapse();
//addAnalyseBotton();
}
function progressMonitor(progressBar, value, maxvalue) {
let pvalue = ~~(value / maxvalue * 100);
progressBar.css('width', pvalue+'%').attr('aria-valuenow', pvalue);
}
function addStyleSettings() {
let reduceContrastChecked = "";
let noWordWrapChecked = "";
if(reduceContrast)
reduceContrastChecked = "checked";
if(noWordWrap)
noWordWrapChecked = "checked";
let title = $("<h3>Styling</h3>");
let row = $('<div class="row"></div>');
let col = $('<div class="col-xs-6"></div>');
let option = $('<sub><input id="reduceContrast" type="checkbox" '+reduceContrastChecked+'></sub> <label for="reduceContrast"> Reduce contrast of comment info box</label>');
option.on("change", function(){updateContrast($("#reduceContrast")[0].checked);});
let col2 = $('<div class="col-xs-6"></div>');
let option2 = $('<sub><input id="noWordWrap" type="checkbox" '+noWordWrapChecked+'></sub> <label for="noWordWrap">Do not wrap words </label>');
option2.on("change", function(){updateWordWrap($("#noWordWrap")[0].checked);});
col.append(option);
col2.append(option2);
row.append(col2);
if($('#chapter-display-options-modal div.modal-body select.form-control[data-type=backgroundColor]').val() === "dark")
row.append(col);
$("#chapter-display-options-modal .modal-body").append(title).append(row);
}
function addGlobalStyle(css) {
let head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
function smartGlossary(dict){
if(rawsReplaced)
return;
let exit = false;
let ofRegExp = /的/g;
let smartDict = [];
for(let i in dict){
let smartReplace = false;
$('.original:contains("'+dict[i].raw+'")').each(function(){
let dictRaw = dict[i].raw;
let raw = this.innerText;
let original = $(this);
let translation = original.prev('.translated');
let entry = new RegExp(dictRaw, 'g');
let numOriginal = raw.match(entry).length;
let traw = "";
translation.find('w,t').each(function(){
traw = traw + $(this).attr( 'data-title' );
});
let numTranslation = traw.count(entry);
let trawClean = traw.replace(entry,"");
if(numOriginal !== numTranslation){
let anchorChars = "";
let prependChars = "";
let appendChars = "";
//check if only "的" were omitted
let testRaw = dictRaw.replace(ofRegExp,"");
if(dictRaw.includes("的") && numOriginal === traw.count(testRaw) && raw.count(testRaw) === 0){
if(traw.count(ofRegExp) + numOriginal <= raw.count(ofRegExp))
anchorChars = testRaw;
dictRaw = testRaw;
}
else{
let xpendMissing = numOriginal - numTranslation;
for(let j = 0; j < dictRaw.length; j++)
{
let char = dictRaw.charAt(j);
let charReg = new RegExp(anchorChars + dictRaw.charAt(j), 'g');
let numOrigChar = raw.count(charReg);
//console.log( "#O " + numOriginal + " #OC " +numOrigChar);
if (numOriginal === numOrigChar){ //use as possible anchor
if (numOriginal === trawClean.count(charReg) + numTranslation){ //is likely safe anchor
anchorChars += char;
continue;
}
}
//check if xpend chars are missing or just relocated.
if(traw.count(char) + xpendMissing > raw.count(char)){
anchorChars=""; //remove anchor, unsafe xpend!
//console.log((traw.count(char) + xpendMissing) + ">" + (raw.count(char))+ " | " + xpendMissing + " ! " + dictRaw +" : "+char+ "\n" + raw);
break;
}
if(anchorChars.length === 0)
prependChars += char;
else{
//if not anchor char it has to be appended
appendChars += char;
//console.log("_"+anchorChars+"$ "+prependChars +" | "+ char);
}
}
}
let safetyCheck = new RegExp("([^" + escapeRegExp(prependChars) + "]|^)" + escapeRegExp(anchorChars) + "([^" + escapeRegExp(appendChars) + "]|$)", 'g');
let safetySize = traw.count(safetyCheck);
if((prependChars.length > 0 || appendChars.length > 0 || dictRaw !== dict[i].raw) && anchorChars.length > 0 && safetySize === (numOriginal - numTranslation)){
smartReplace = true;
let type = dict[i].overwrite === "TRUE" ? '':'w';
translation.find(type+'[data-title^='+anchorChars.charAt(0)+']').each(function(){
let node = $(this);
let rawattr = node.attr("data-title");
let toCleanup = [];
if(dict[i].overwrite === "TRUE"){
type = 'w, t';
}
while(rawattr.length < anchorChars.length && node.next(type).size() > 0){
let nextNode = node[0].nextSibling; //to check later for textnodes
node = node.next(type);
rawattr += node.attr("data-title");
// skip fill words from translation like "the", "can", whitespaces...
while (nextNode && nextNode.nodeType == 3) {
toCleanup.push(nextNode);
nextNode = node.nextSibling;
}
toCleanup.push(node);
}
if(rawattr == anchorChars){
let textPlaceholder = "";
for(let j in toCleanup){
textPlaceholder += $(toCleanup[j]).text();
toCleanup[j].remove();
}
if(textPlaceholder.length >0)
$(this).text($(this).text() + textPlaceholder);
$(this).attr('data-title', dict[i].raw);
}
});
}
//console.log(numOriginal + " <> " + numTranslation + ": " + dictRaw + " || " + anchorChars + " + " + appendChars);
}
});
for(let j in smartDict)
{
if(smartReplace === true)
break;
if (dict[i].raw.includes(smartDict[j].raw))
smartReplace = true;
}
if(smartReplace === true)
smartDict.push(dict[i]);
}
//replace terms for "fixed" Raws
replaceTerms(smartDict);
}
function rawsReplace(){
if(replaceInOriginal) {
$('.original t').each(function(){let mytext = $(this).text(); $(this).text($(this).attr('data-title').replace(/\(\w+\)/g,'')); $(this).attr('data-title', mytext);});
// add whitespace between two english words
$('.original').find('t').filter( function(index){var prev = $(this).get(0).previousSibling; return prev ? $(this).get(0).previousSibling.nodeName == 'T' : false;}).each(function(){$(this).text(' ' + $(this).text());});
rawsReplaced = true;
}
}
const OVERWRITE = {overwrite:"TRUE"};
function applyGlossaryAsync(dict, changeCount){
if (autoSolveConflicts)
dict = dict.map( item => { return {...item, ...OVERWRITE}; });
if (changeCount == 0)
dict = [];
dict = dict.slice(-changeCount);
return new Promise( resolve => {
setTimeout(async function(){
for(i = 0; i*glossaryChunkSize < dict.length; i++){
applyGlossary(dict.slice(i*glossaryChunkSize, (i+1)*glossaryChunkSize), true);
if ((i+1)*glossaryChunkSize < dict.length)
await sleep(300);
}
resolve();
}, 1);
}).then(() => console.log("Latest LNMTL glossary entries applied (" + dict.length + " terms)"));
}
function waitApplyGlossary(dict, skipReplace){
return new Promise( resolve => {
LNMTLGlossaryLock.then(() => {
applyGlossary(dict, skipReplace);
resolve();
});
});
}
function applyGlossary(dict, skipReplace){
replaceTerms(dict);
smartGlossary(dict);
// original replace
if(replaceInOriginal && !skipReplace && !rawsReplaced) {
for(let i in dict){
$('.original:contains("'+dict[i].raw+'")').find(':contains("'+dict[i].raw+'")').andSelf().contents().each(function() {
let text = $(this).text();
if(this.nodeType == 3){
$(this).replaceWith(text.replace(new RegExp(dict[i].raw, "g"), ' '+dict[i].meaning+' '));
}
else if(dict[i].overwrite === "TRUE" && $(this).is('t')){
if($(this).text() == dict[i].raw)
$(this).attr('data-title', dict[i].meaning);
}
});
}
}
}
function replaceTerms(dict){
let simplereplace = false;
for(let i in dict){
//$('.translated').find('w[data-title='+dict[i].raw+']').text(dict[i].meaning);
dict[i].raw = dict[i].raw.trim();
if(dict[i].raw.length === 0){
continue;
}
let type = 'w';
if(dict[i].overwrite === "TRUE"){
type = '';
}
$('.translated '+type+'[data-title^='+dict[i].raw.charAt(0)+']').each(function(){
let node = $(this);
let raw = node.attr("data-title");
if(simplereplace && raw == dict[i].raw){
node.text(dict[i].meaning);
return;
}
let toCleanup = [];
if(dict[i].overwrite === "TRUE"){
type = 'w, t';
}
while(raw.length < dict[i].raw.length && node.next(type).size() > 0){
let nextNode = node[0].nextSibling; //to check later for textnodes
node = node.next(type);
// console.log(node.text() + node.attr("data-title"));
raw += node.attr("data-title");
// skip fill words from translation like "the", "can", whitespaces...
while (nextNode && nextNode.nodeType == 3) {
// console.log("textnode: " + nextNode.data);
toCleanup.push(nextNode);
nextNode = node.nextSibling;
}
toCleanup.push(node);
}
if(raw == dict[i].raw){
$(this).text(dict[i].meaning);
for(let j in toCleanup){
toCleanup[j].remove();
}
let text = dict[i].meaning;
if(this.previousSibling.nodeValue === ". " || $(this).prev().size() === 0 && ($(this).parent('sentence').size() > 0 || ($(this).parent('dq').size() > 0 && $(this).parent('dq').prev().size() === 0 ))){
text = capitalize(text);
}
let translate = $("<t></t>").text(text);
translate.attr("data-title", dict[i].raw);
$(this).after(translate);
$(this).remove();
//fix click
translate.click(function(){ $('t').popover({trigger: 'click', placement: 'top', html : true });});
}
});
}
}
function novelSheet(tabletop, novelname){
let sheetnames = tabletop.modelNames.join('|').toLowerCase().replace(/ /g,"-").split('|');
let sheetindex = sheetnames.indexOf(novelname);
if (sheetindex !== -1) {
let sheetname = tabletop.modelNames[sheetindex];
if(tabletop.sheets(sheetname) !== undefined){
return waitApplyGlossary(tabletop.sheets(sheetname).elements).then(() => console.log("Applied glossary: " + sheetname));
}
}
return Promise.resolve();
}
function useData(data, tabletop) {
let sheets = Object.keys(data);
if(sheets.length === 1){
applyGlossary(tabletop.sheets(sheets[0]).elements);
return;
}
let novelname = window.location.pathname.split("/").pop();
novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
let book_index = novelname.lastIndexOf("-book-");
if(book_index > novelname.length - 10 && book_index > 0){
novelname = novelname.substr(0, book_index);
}
novelSheet(tabletop, novelname).then(() => novelSheet(tabletop, "global")).then(rawsReplace());
}
function applySetting(){
changeWordWrap(noWordWrap);
changeContrast(reduceContrast);
}
function addCustomStyles(){
// no word wrapping
addGlobalStyle('body .chapter-body.noWordWrapping { hyphens: none;}');
// reduced contrast (of comment box)
addGlobalStyle('.reducedContrast.backgroundColorDark .alert-info , .reducedContrast.backgroundColorBlack .alert-info { background-color: #2d323f; border-color: #145461}');
// bugfixes
addGlobalStyle('.backgroundColorBright .tt-input { color: #464444 !important;}');
addGlobalStyle('.fontColorGray .chapter-body w.postfixed { color: #95a597 !important; }');
addGlobalStyle('.fontColorGray .chapter-title {color: #9b9b9b;}');
// glossary analyzer
addGlobalStyle('.progress-bar-animated {animation: progress-bar-stripes 1s linear infinite;}');
addGlobalStyle('.backgroundColorBlack #novel-display-conflicts-modal .table>tbody>tr.active>td, .backgroundColorBlack #novel-display-conflicts-modal .table>tbody>tr.active>th { background-color: #DDD !important; padding: 1px !important;}');
addGlobalStyle('.backgroundColorDark #novel-display-conflicts-modal .table>tbody>tr.active>td, .backgroundColorDark #novel-display-conflicts-modal .table>tbody>tr.active>th { background-color: #DDD !important; padding: 1px !important;}');
}
function changeWordWrap(on){
if(on === true)
$('body .chapter-body').addClass("noWordWrapping");
if(on === false)
$('body .chapter-body').removeClass("noWordWrapping");
}
function changeContrast(on){
if(on === true)
$('body').addClass("reducedContrast");
if(on === false)
$('body').removeClass("reducedContrast");
}
(function() {
'use strict';
addCustomStyles();
if (!disableGlossaryAnalyzer && $("#navbar a:contains('Login')").length == 0)
addAnalyseButton();
applySetting();
addCloseSettingsListener();
addUserGlossaryLink();
addGlossarySettings();
addStyleSettings();
if ((window.location.pathname.startsWith("/novel/") || window.location.pathname.startsWith("novel/"))
&& ($("#navbar a:contains('Login')").length == 0 && (useLNMTLGlossary || !disableGlossaryAnalyzer))) {
let data = ":root";
let url = window.location.toString();
let novelData = restore("ndata" + url);
let timeNow = new Date().getTime();
if (novelData === null )
novelData = { changes: 0 };
console.log("update novel metadata: " + url);
let old_changes = novelData.changes;
let old_retrans = novelData.retrans;
let novelinfo = $('.panel.panel-default .progress-bar[role="progressbar"]', $(data));
let glossary_changes = novelinfo.text().trim().split(' / ');
let retranslation_time = new Date($(".panel.panel-default dt:contains('Latest retranslation at') ~ * span", $(data)).text()).getTime();
let novel_id = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']", $(data)).attr("href").split("https://lnmtl.com/termProposition?novel_id=")[1];
novelData.time = timeNow;
novelData.changes = glossary_changes[0];
novelData.retrans = retranslation_time;
novelData.novel_id = novel_id;
store("ndata" + url, novelData);
if (novelData.changes - old_changes != 0 || old_retrans !== novelData.retrans)
localStorage.removeItem("gdata" + novelData.novel_id);
if (!disableGlossaryAnalyzer)
addGlossaryAnalyseModal();
}
if (window.location.pathname.startsWith("/chapter/") || window.location.pathname.startsWith("chapter/")){
let novelname = window.location.pathname.split("/").pop();
novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
let book_index = novelname.lastIndexOf("-book-");
if(book_index > novelname.length - 10 && book_index > 0){
novelname = novelname.substr(0, book_index);
}
if (useLNMTLGlossary)
LNMTLGlossaryLock = lock(() => loadLNMTLGlossary("https://lnmtl.com/novel/" + novelname));
fetch_glossary(); //google docs glossary
}
})();