oer-ai-examples/perchance-christus

4811 lines
557 KiB
Plaintext
Raw Normal View History

<!DOCTYPE html> <html> <meta charset="utf-8"> <title>Perchance</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <base href="https://perchance.org/" target="_parent"> <meta name="referrer" content="no-referrer"> <style> html{ font-family:sans-serif; -ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; } body{ margin:0;} article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section{ display:block;} audio, canvas, progress, video{ display:inline-block; vertical-align:baseline; } audio:not([controls]){ display:none;height:0;} [hidden], template{ display:none;} a{ background-color:transparent;} a:active, a:hover{ outline:0;} abbr[title]{ border-bottom:1px dotted;} b, strong{ font-weight:bold;} dfn{ font-style:italic;} h1{ font-size:2em;margin:0.67em 0;} mark{ background:#ff0;color:#000;} small{ font-size:80%;} sub, sup{ font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{ top:-0.5em;}sub{ bottom:-0.25em;} img{ border:0;} svg:not(:root){ overflow:hidden;} figure{ margin:1em 40px;} hr{ box-sizing:content-box;height:0;} pre{ overflow:auto;} code, kbd, pre, samp{ font-family:monospace, monospace;font-size:1em;} button:not([disabled]), input:not([disabled]), optgroup:not([disabled]), select:not([disabled]), textarea:not([disabled]){ color:inherit; }button, input, optgroup, select, textarea{ font:inherit; margin:0; } button{ overflow:visible;} button, select{ text-transform:none;} button, html input[type="button"], input[type="reset"], input[type="submit"]{ -webkit-appearance:button; cursor:pointer; } button[disabled], html input[disabled]{ cursor:default;} button::-moz-focus-inner, input::-moz-focus-inner{ border:0;padding:0;} input{ line-height:normal;} input[type="checkbox"], input[type="radio"]{ box-sizing:border-box; padding:0; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button{ height:auto;} input[type="search"]{ -webkit-appearance:textfield; box-sizing:content-box; } input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration{ -webkit-appearance:none;} fieldset{ border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;} legend{ border:0; padding:0; } textarea{ overflow:auto;} optgroup{ font-weight:bold;} table{ border-collapse:collapse;border-spacing:0;}td, th{ padding:0;}</style> <script>try{window.localStorage.testIfWeCanReadLocalStorageWithoutDOMError}catch(err){console.log("localStorage is disallowed. Adding a 'dummy' polyfill.");Object.defineProperty(window,"localStorage",{value:new Proxy({},{set:(t,k,v)=>t[k]=String(v)})})}</script> <script>let __localToken = "LOCAL"; // downloader changes this to "LOCAL"
function __inIframe () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
// to prevent *direct* null.perchance.org / <publicId>.perchance.org access (but let it run however if it's a local copy):
if(!__inIframe() && __localToken !== "LOCAL"){window.location.href="https://perchance.org/yz31uwjhue";} // not to self: don't change this without changing generator downloader script</script> <script>// POLYFILLS:
if(!Array.prototype.at) { // For Safari 14
Array.prototype.at = function(relativeIndex) {
let i = relativeIndex >= 0 ? relativeIndex : this.length + relativeIndex;
return this[i];
};
}</script> <script>window.__ignorePerchanceErrors=!1,window.generatorName="yz31uwjhue",window.generatorPublicId="e8a155a6f0e16d976bc0608fcb333ac7",window.generatorLastEditTime=Number("1717599511371"),window.__parentWindow=window.parent;</script> <style> html{ margin:0;padding:0;width:100%;height:100%;box-sizing:border-box;}*, *:before, *:after{ box-sizing:inherit;}body, #output-container{ margin:0;padding:0;text-align:center;}#output-container{ height:100%;}hidden{ position:fixed;top:-14000px;left:-14000px;} #output-container ul{ text-align:left;}</style> </head> <body> <script id="preloaded-generator-data" type="notjs">%7B%22name%22:%22yz31uwjhue%22,%22modelText%22:%22image%20=%20%7Bimport:text-to-image-plugin%7D%5Cn%5Cntitle%20%5Cn%20%20Jesus%20Christus%5Cn%5Cncharacter%5Cn%20%20Jesus%20Christus%5Cn%5Cnitem%5Cn%20%20a%20bag%20with%20coins%5Cn%20%20a%20long%20staff%5Cn%20%20a%20sword%5Cn%20%20a%20red%20line%5Cn%20%20fire%5Cn%20%20bread%20and%20fishes%5Cn%20%20bread%20and%20wine%5Cn%20%20good%20smelling%20oil%5Cn%5Cnadjective%5Cn%20%20compassionate%5Cn%20%20serene%5Cn%20%20resolute%5Cn%20%20sorrowful%5Cn%5Cnpack%5Cn%20%20bag%5Cn%20%20sack%5Cn%20%20none%5Cn%5Cnplace%5Cn%20%20Jerusalem%5Cn%20%20Golgatha%5Cn%20%20the%20Temple%5Cn%20%20in%20the%20sky%5Cn%20%20a%20garden%5Cn%20%20in%20Hades%5Cn%20%20the%20Last%20Supper%5Cn%5Cntime%5Cn%20%20night%5Cn%20%20day%5Cn%5Cnprompt%5Cn%20%20detailed%20medieval%20painting%20of%20%5Bcharacter%5D,%20%5Badjective%5D,%20in%20%5Bplace%5D%20at%20%5Btime%5D,%20holding%20%5Bitem%5D.%20%5Cn%20%20In%20the%20style%20of%20Caravaggio,%20Piero%20della%20Francesca,%20Michelangelo,%20Leonardo%20da%20Vinci,%20Rembrandt,%20or%20Lukas%20Cranach.%20%5Cn%20%20Intricate%20brushwork,%20rich%20colors,%20dramatic%20lighting,%20classical%20style,%20oil%20on%20canvas,%20realism,%20fine%20details,%20high%20resolution,%20depth%20of%20field%5Cn%20%20resolution%20=%20512x768%5Cn%5Cnoutput%5Cn%20%20%5Bimage(prompt)%5D%5Cn%22,%22outputTemplate%22:%22%3Ch1%3E%5Btitle%5D%3C/h1%3E%5Cn%3Cp%20style=%5C%22margin:1em%20auto;%20padding:0%201em;%20max-width:768px;%5C%22%3E%5Boutput%5D%3C/p%3E%5Cn%3Cinput%20oninput=%5C%22attribute%20=%20this.value%5C%22%20placeholder=%5C%22Gib%20ein%20zus%C3%A4tzliches%20Attribut%20an%5C%22%20onChange=%5C%22update()%5C%22/%3E%5Cn%3Cbutton%20onclick=%5C%22update()%5C%22%3EGeneriere%3C/button%3E%5Cn%5Cn%3Cbr%3E%5Cn%3C!--%20Learn%20HTML%20here:%20%20%20https://www.khanacademy.org/computing/computer-programming/html-css%20%20%20%20--%3E%5Cn%22,%22imports%22:%5B%22text-to-image-plugin%22%5D,%22canLink%22:false,%22isPrivate%22:false%7D</script> <script id="imported-generators" type="notjs">%5B%7B%22name%22:%22text-to-image-plugin%22,%22modelText%22:%22%5Cn//%20NOTE%20TO%20SELF:%20If%20you%20add%20more%20properties,%20make%20sure%20you%20add%20to%20the%20regex%20below,%20and%20the%20variable%20declarations%20in%20each%20'branch'%5Cn%5Cn$output(data)%20=%3E%5Cn%20%20if(data%20===%20undefined)%20return%20%5C%22(Error:%20you've%20input%20an%20empty%20value/variable%20into%20the%20text-to-image-plugin)%5C%22;%5Cn%20%20//%20if(options%20===%20undefined)%20options%20=%20%7B%7D;%5Cn%20%20%5Cn%20%20let%20serverOrigin%20=%20%5C%22https://image-generation.perchance.org%5C%22;%5Cn%20%20%5Cn%20%20let%20evaluatedInputs;%5Cn%5Cn%20%20////////////////////////////////////////////////%5Cn%20%20//%20%20%20%20%20%20%20set%20up%20handler%20for%20gallery%20%20%20%20%20%20%20%20%20%20%20//%5Cn%20%20////////////////////////////////////////////////%5Cn%5Cn%20%20if(!window.___addedTextToImagePluginFirstTimeCode98420274)%20%7B%5Cn%20%20%20%20window.addEventListener(%5C%22message%5C%22,%20function(e)%20%7B%5Cn%20%20%20%20%20%20let%20origin%20=%20e.origin%20%7C%7C%20e.originalEvent.origin;%20//%20For%20Chrome,%20the%20origin%20property%20is%20in%20the%20event.originalEvent%20object.%5Cn%20%20%20%20%20%20if(origin%20!==%20serverOrigin)%20%7B%5Cn%20%20%20%20%20%20%20%20return;%5Cn%20%20%20%20%20%20%7D%5Cn%20%20%20%20%20%20if(e.data.openGallerySignal)%20%7B%5Cn%20%20%20%20%20%20%20%20let%20ctn%20=%20document.createElement(%5C%2
github.com/nlp-compromise/compromise
MIT
*/
!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).nlp=t()}(function(){return function i(o,s,u){function l(e,t){if(!s[e]){if(!o[e]){var r="function"==typeof require&&require;if(!t&&r)return r(e,!0);if(c)return c(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var a=s[e]={exports:{}};o[e][0].call(a.exports,function(t){return l(o[e][1][t]||t)},a,a.exports,i,o,s,u)}return s[e].exports}for(var c="function"==typeof require&&require,t=0;t<u.length;t++)l(u[t]);return l}({1:[function(h,r,n){(function(e){!function(t){"object"==typeof n&&void 0!==r?r.exports=t():("undefined"!=typeof window?window:void 0!==e?e:"undefined"!=typeof self?self:this).unpack=t()}(function(){return function i(o,s,u){function l(e,t){if(!s[e]){if(!o[e]){var r="function"==typeof h&&h;if(!t&&r)return r(e,!0);if(c)return c(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var a=s[e]={exports:{}};o[e][0].call(a.exports,function(t){return l(o[e][1][t]||t)},a,a.exports,i,o,s,u)}return s[e].exports}for(var c="function"==typeof h&&h,t=0;t<u.length;t++)l(u[t]);return l}({1:[function(t,e,r){"use strict";var s=36,i="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",u=i.split("").reduce(function(t,e,r){return t[e]=r,t},{});e.exports={toAlphaCode:function(t){if(void 0!==i[t])return i[t];for(var e=1,r=s,n="";r<=t;t-=r,e++,r*=s);for(;e--;){var a=t%s;n=String.fromCharCode((a<10?48:55)+a)+n,t=(t-a)/s}return n},fromAlphaCode:function(t){if(void 0!==u[t])return u[t];for(var e=0,r=1,n=s,a=1;r<t.length;e+=n,r++,n*=s);for(var i=t.length-1;0<=i;i--,a*=s){var o=t.charCodeAt(i)-48;10<o&&(o-=7),e+=o*a}return e}}},{}],2:[function(t,e,r){"use strict";var o=t("./unpack");e.exports=function(t){var a=t.split("|").reduce(function(t,e){var r=e.split("¦");return t[r[0]]=r[1],t},{}),i={};return Object.keys(a).forEach(function(t){var e=o(a[t]);"true"===t&&(t=!0);for(var r=0;r<e.length;r++){var n=e[r];!0===i.hasOwnProperty(n)?!1===Array.isArray(i[n])?i[n]=[i[n],t]:i[n].push(t):i[n]=t}}),i}},{"./unpack":4}],3:[function(t,e,r){"use strict";var a=t("../encoding");e.exports=function(t){for(var e=new RegExp("([0-9A-Z]+):([0-9A-Z]+)"),r=0;r<t.nodes.length;r++){var n=e.exec(t.nodes[r]);if(!n){t.symCount=r;break}t.syms[a.fromAlphaCode(n[1])]=a.fromAlphaCode(n[2])}t.nodes=t.nodes.slice(t.symCount,t.nodes.length)}},{"../encoding":1}],4:[function(t,e,r){"use strict";var n=t("./symbols"),p=t("../encoding");e.exports=function(t){var m,d,e={nodes:t.split(";"),syms:[],symCount:0};return t.match(":")&&n(e),m=e,d=[],function t(e,r){var n,a,i,o,s=m.nodes[e];"!"===s[0]&&(d.push(r),s=s.slice(1));for(var u=s.split(/([A-Z0-9,]+)/g),l=0;l<u.length;l+=2){var c=u[l],h=u[l+1];if(c){var f=r+c;","!==h&&void 0!==h?t((n=m,a=h,i=e,(o=p.fromAlphaCode(a))<n.symCount?n.syms[o]:i+o+1-n.symCount),f):d.push(f)}}}(0,""),d}},{"../encoding":1,"./symbols":3}]},{},[2])(2)}),function(t){"object"==typeof n&&void 0!==r?r.exports=t():("undefined"!=typeof window?window:void 0!==e?e:"undefined"!=typeof self?self:this).unpack=t()}(function(){return function i(o,s,u){function l(e,t){if(!s[e]){if(!o[e]){var r="function"==typeof h&&h;if(!t&&r)return r(e,!0);if(c)return c(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var a=s[e]={exports:{}};o[e][0].call(a.exports,function(t){return l(o[e][1][t]||t)},a,a.exports,i,o,s,u)}return s[e].exports}for(var c="function"==typeof h&&h,t=0;t<u.length;t++)l(u[t]);return l}({1:[function(t,e,r){"use strict";var i="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",s=i.split("").reduce(function(t,e,r){return t[e]=r,t},{});e.exports={toAlphaCode:function(t){if(void 0!==i[t])return i[t];for(var e=1,r=36,n="";r<=t;t-=r,e++,r*=36);for(;e--;){var a=t%36;n=String.fromCharCode((a<10?48:55)+a)+n,t=(t-a)/36}return n},fromAlphaCode:function(t){if(void 0!==s[t])return s[t];for(var e=0,r=1,n=36,a=1;r<t.length;e+=n,r++,n*=36);for(var i=t.length-1;0<
function __addNodeMethods(node) {
// REMEMBER TO UPDATE stringAndNumberModifications and arrayModifications if you add more methods
Object.defineProperty(node, "$odds", {get:__$oddsMethod, configurable:true});
Object.defineProperty(node, "getOdds", {get:__$oddsMethod, configurable:true});
Object.defineProperty(node, "getName", {get:function() { return this.$text; }, configurable:true});
Object.defineProperty(node, "getParent", {get:function() { return this.$parent; }, configurable:true});
// This is incorrect because it counts properties that have been added by the user at runtime with code like `list.foo = 10`
// Object.defineProperty(node, "getLength", {get:function() { return Object.keys(this).length; }, configurable:true});
Object.defineProperty(node, "getLength", {get:function() { return this.$children.length; }, configurable:true});
Object.defineProperty(node, "getRawListText", {get:function() { return this.$perchanceCode; }, configurable:true});
// Workaround for the problem of a node not being resolved (mainly for use in functions) (I put this in the execution properties of the proxy)
Object.defineProperty(node, "getSelf", {get:function() { return this; }, configurable:true});
Object.defineProperty(node, "getPropertyKeys", {get:function() { return this.$valueChildren.slice(0); }, configurable:true}); // deprecated, but staying for backwards-compat. use getPropertyNames instead
Object.defineProperty(node, "getPropertyNames", {get:function() { return this.$valueChildren.slice(0); }, configurable:true});
Object.defineProperty(node, "getChildNames", {get:function() { return this.$children.slice(0); }, configurable:true});
Object.defineProperty(node, "getFunctionNames", {get:function() { return this.$functionChildren.slice(0); }, configurable:true});
Object.defineProperty(node, "getAllKeys", {get:function() { return this.$allKeys.slice(0); }, configurable:true});
// Object.defineProperty(node, "isConsumable", {value:false, writable:true, configurable:true})
Object.defineProperty(node, "toString", {value:__toStringMethod, configurable:true});
// we need this otherwise if they have:
// num = {1-1000}
// and they type [num.toLocaleString()], the JS engine calls toString on the object (since that's the default thing to do with objects, I guess), which results in a string.
Object.defineProperty(node, "toLocaleString", {value:function(...args) {
let result = String(this.toString()); // String()-ing because apparently toString can return a number?? no time to look into it right now
if(String(Number(result)) === result) {
result = Number(result);
}
return result.toLocaleString(...args);
}, writable:true, configurable:true});
Object.defineProperty(node, "evaluateItem", {get:function() {
return this.valueOf();
}, configurable:true});
// TODO: add a setter method for $odds which changes the node data? wait, shouldn't $odds just return the string? or not?
// TODO: these need fixing (see funciton declaration for notes)
// Object.defineProperty(node, "$addChild", {get:$addChildMethod});
// Object.defineProperty(node, "$removeChild", {get:$removeChildMethod});
Object.defineProperty(node, "selectOne", {get:__selectOneMethod, configurable:true});
Object.defineProperty(node, "selectAll", {get:__selectAllMethod, configurable:true});
Object.defineProperty(node, "selectMany", {value:__selectManyMethod, writable:true, configurable:true});
Object.defineProperty(node, "selectUnique", {value:__selectUniqueMethod, writable:true, configurable:true});
Object.defineProperty(node, "joinItems", {value:__joinItemsMethod, writable:true, configurable:true});
Object.defineProperty(node, "sumItems", {get:function() {
return this.selectAll.reduce((a, v) => a+v.evaluateItem, 0);
}, configurable:true});
Object.defineProperty(node, "valueOf", {value:__valueOfMethod, writable:true, configurable:true});
//Object.defineProperty(node, "shuffleItems", {value:shuffleMethod, configurable:true});
// TEXT TRANSFORMS:
Object.defineProperty(node, "pluralForm", {get:__pluralFormMethod, configurable:true});
Object.defineProperty(node, "singularForm", {get:__singularFormMethod, configurable:true});
Object.defineProperty(node, "pastTense", {get:__pastTenseMethod, configurable:true});
Object.defineProperty(node, "presentTense", {get:__presentTenseMethod, configurable:true});
Object.defineProperty(node, "futureTense", {get:__futureTenseMethod, configurable:true});
Object.defineProperty(node, "negativeForm", {get:__negativeFormMethod, configurable:true});
// TODO**: should have seperate "soft" versions of sentence and title case which DON'T call toLowerCase on the whole string first? (i.e. to leave current capitalisations there)
Object.defineProperty(node, "sentenceCase", {get:__sentenceCaseMethod, configurable:true});
Object.defineProperty(node, "titleCase", {get:__titleCaseMethod, configurable:true});
Object.defineProperty(node, "lowerCase", {get:__lowerCaseMethod, configurable:true});
Object.defineProperty(node, "upperCase", {get:__upperCaseMethod, configurable:true});
Object.defineProperty(node, "replaceText", {value:__replaceTextMethod, configurable:true});
// REMEMBER: add text transform names here if you add new ones:
if(textTransformNames.length === 0) {
textTransformNames.push("pluralForm");
textTransformNames.push("singularForm");
textTransformNames.push("pastTense");
textTransformNames.push("presentTense");
textTransformNames.push("futureTense");
textTransformNames.push("negativeForm");
textTransformNames.push("sentenceCase");
textTransformNames.push("titleCase");
textTransformNames.push("lowerCase");
textTransformNames.push("upperCase");
textTransformNames.push("replaceText");
}
//Object.defineProperty(node, "createClone", {get:createCloneMethod, configurable:true});
Object.defineProperty(node, "consumableList", {get:__consumableListMethod, configurable:true});
Object.defineProperty(node, "createClone", {get:function() { return __duplicatePerchanceNode(this); }, configurable:true});
// Object.defineProperty(node, "ordinal", {get:ordinalMethod});
// Object.defineProperty(node, "$withArticle", {get:withArticleMethod});
// TODO: add the rest of the default methods for version 1 (including getters like ".itemCount" (which gets the length of the $children array))
// TODO: .hideOutput transform?
// TODO: .replaceText("blah","na") transform (alias for .replace() except it always replaces ALL occurances unless you use regex and not the global flag)
// TODO: add any newly added execution properties to the proxy executionProperties array (e.g. replaceText)
// TODO***: remember that you'll need to move the `apply`s along with the text transforms that have an `apply` (e.g. replaceText)
}
let __replaceTextMethod = function(needle, replacement) {
if(needle instanceof RegExp) {
return this.toString().replace(needle, replacement.toString());
} else {
//needle = new RegExp(needle.toString(), "g");
//return this.toString().replace(needle, replacement.toString());
return this.toString().split(needle.toString()).join(replacement.toString());
}
}
// let createCloneMethod = function() {
// // TODO*****: This would try to clone $moduleSpace, $parent, $root, etc.???????????????????!!!!!!!!!!!!!!!!
// // --> test by putting in debugger and testing `this.$parent === clone.parent`
// // --> need to temporarily remove properties which reference other node objects directly? then add them back after
// // At least it DOESN'T make copies of functions
// // I think I need to make my own cloner... either that or really make sure this is working like I expect
// let clone = cloner.deep.copy(this);
// if(!this.___isLocalCall) {
// return clone;
// } else {
//
// let proxiedThisRef;
// proxiedThisRef = new Proxy(clone, {
// // NOTE: if you edit this, edit the same one in evaluateSquareBlock
// get: function(target, property, receiver) {
// if(property === "$output") {
// return undefined;
// } else if(property === "___isLocalCall") {
// return true;
// } else {
//
// let desc = Object.getOwnPropertyDescriptor(target, property);
// if(!desc) {
// perchanceError(`The '${property}' property doesn't exist within '${target.getName}'.`, ctxInfo.declarationLineNumber);
// return;
// }
// if(desc.get) {
// return desc.get.bind(proxiedThisRef)();
// } else if(desc.value && typeof desc.value === 'function') {
// return desc.value.bind(proxiedThisRef);
// } else {
// return target[property];
// }
//
// }
// }
// });
//
// return proxiedThisRef;
//
// }
// };
let __consumableListMethod = function() {
// it's called consumable*List* because it only makes sense to call it on a list-oriented node
// let list = this.createClone;
// Object.defineProperty(list, "isConsumable", {value:true, configurable:true, writable:true}); // not $-prefixed because it should be publicly changable
// return list;
// If this node is wrapped in the $output-hider (See REF:kjhfw927f63ohwkgw82g in evaluateSquareBlock.js), then we unwrap it and do the unhiding ourself in the proxy we create below.
// This is needed due to this problem: https://www.reddit.com/r/perchance/comments/fx7exs/how_to_have_multiple_lists_point_to_the_same/fmxzccb/ Basically, if we have something like `$output = [this.consumableList.selectMany(3)]`, then, target[property] will be returned (in the below proxy's getter) *bound to the $output-hider proxy* rather than this one, and so there is no consumableList from the perspective of selectMany. So we make this proxy handle the $output hiding stuff. Remember than binding is different to proxying! This is some chef's kiss spaghetti
let thisRef = this;
let $outputShouldBeHidden = false;
if(thisRef.___$outputShouldBeHidden) {
thisRef = thisRef.___proxyTarget;
$outputShouldBeHidden = true;
}
let proxy = new Proxy(thisRef, {
alreadyConsumedItems:new Set(),
get: function(target, property) {
if(property === "selectOne") {
// NOTE: inline consumable lists are handled in the toStringMethod
let selectedNode = __selectOneMethod.bind(proxy)();
if(selectedNode && selectedNode.$nodeType) { // make sure it's a node (could be an error string)
this.alreadyConsumedItems.add(selectedNode);
}
return selectedNode;
} else if(property === "evaluateItem") {
return __valueOfMethod.bind(proxy)();
} else if(property === "getLength") {
if(Object.keys(target).includes("getLength")) return target["getLength"];
else return target.$children.length - this.alreadyConsumedItems.size;
// else return Object.keys(target).length - this.alreadyConsumedItems.size;
} else if(property === "$alreadyConsumedItems") {
return this.alreadyConsumedItems;
} else if(textTransformNames.includes(property)) {
return proxy.selectOne[property]; // added due to this problem: https://www.reddit.com/r/perchance/comments/7uw0sp/errors_with_consumablelist_and_uppercase_together/
} else if(property === "getSelf") {
return proxy;
} else if($outputShouldBeHidden && property === "$output") {
return undefined;
} else {
// TODO: strangely, the return value here seems to be automatically bound to this proxy.
// so it also handles toString and valueOf methods like we want it to.
// I think it's because in `foo().bar()`, bar is by default bound to the thing returned by foo?
return target[property];
}
},
});
return proxy;
};
let __valueOfMethod = function(key) {
let str = this.toString();
if(String(Number(str)) === str) {
return Number(str);
} else {
return str;
}
};
// let idMethod = function(key) {
//
// if(typeof key !== 'string') {
// key = key.toString();
// }
//
// // TODO***: (BIG ONE): clone `this` and set its toString method so it
// // always returns the $child string that was selected below BUT ALSO can
// // still have its sub-properties accessed. (i.e. the only difference between
// // the clone and the original is that the clone always returns a static string
// // when toString is called (instead of selecting a random child - or returning the
// // evaluated $value text in the case of a $value node))
// // BUT HERE'S THING THING: if a child node is selected and it's "the {cat|dog}", do
// // future requests to that id return "the {cat|dog}" or the EVALUATED version of
// // that text? So we can store either:
// // - the node itself
// // - the text of the node
// // - the *evaluated* text of the node
// // and return it next time that id is called again. Which one? The ideal one seems
// // to depend on the context:
// /*
//
// // here, the node one doesn't make sense, but the other two work fine
// animal
// dog
// cat
// pig
//
// // here, the node one doesn't make sense, and the other two behave differently.
// // it seems like storing the *evaluated* text would make the most sense in this case.
// sentence
// I'm {going|running} to the {shops|hotel}.
// That's a {great|silly|quaint} handbag you've got there.
// // and actually, I can't think of an example where storing the un-evaluated text would
// // make sense.
//
// NEW TACK: curly notation for the id thing because it will be so common:
// {animal#1} -> [animal.$id(1)]
// but wait - I still need to decide on id behavior...
// -----
// The cloning behaviour makes the most sense? Need to bite the bullet and spend a day perfecting this.
// It's an integral part of the last big trio (id/unique/remember)
//
//
// */
//
// if(this.$idStore[key]) {
// return this.$idStore[key];
// } else {
// let child = this.$child;
//
// // if its text is dynamic, we need to evaluate it (they expect the same text each time!).
// // if it's not, there's no need to evaluate it.
// // note that if it IS dynamic, then there won't be any child properties, so we can just store
// // and return a plain old string!
// let blocks = splitTextAtAllBlocks(child.$text);
// if(blocks.length > 1 || blocks[0][0] === "[" || blocks[0][0] === "{") {
// child = child.toString();
// }
//
// this.$idStore[key] = child;
// return this.$idStore[key];
// }
//
// };
// let setIdMethod = function(key) {
// if(this.$idStore[key]) {
// delete this.$idStore[key];
// }
// return this.$id(key);
// };
let __selectAllMethod = function() {
// This methos gets ALL items, regarless of whether they have odds == 0
// If one wants to exlude `odds==0` items: list.selectAll.filter(item => item.getOdds > 0)
let $output = this.$output;
if($output !== undefined) {
return [$output]; // make it an array because why not. (consistency is nice)
}
if(this.$nodeType === "value") {
let text = this.$value;
if(text[0] === "{" && text[text.length-1] === "}") {
let items = __splitUpCurlyOrBlock(text.substr(1, text.length-2));
if(items) { // i.e. if it's a valid curly OR block
return items;
} else {
__perchanceError(`You've called 'selectAll' on something that doesn't appear to be a list?`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
return;
}
} else {
__perchanceError(`You've called 'selectAll' on something that doesn't appear to be a list?`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
return;
}
}
let arr = [];
for(let key of this.$children) {
arr.push(this[key]);
}
arr.toString = function() { return this.join(""); }; // This makes sense, right? Who would selectAll, but then expect a single random item? That would be silly.
return arr;
};
let __selectOneMethod = function() {
const isConsumableList = !!this.$alreadyConsumedItems;
let consumedNodes = new Set();
if(isConsumableList) {
consumedNodes = this.$alreadyConsumedItems; // this is a property of the consumableList proxy handler that we expose
}
let $output = this.$output; // cache it so we don't "execute" it twice if it's something like `$output = [this.age++]`
if($output !== undefined) {
return $output;
}
if(this.$nodeType === "value") {
// I'm pretty sure we want to `toString` (evaulate) it because why would you call selectOne on a value node otherwise?
// Consider these two examples:
// height = {1-10}
// height
// {1-10}
// You might think we should treat the former the same way as the latter (i.e. NOT evaluate it), but that doesn't make sense in terms of the user's intention.
// I'm pretty sure I've made the right decision here but hopefully the beta testers will surface any mistakes in my reasoning.
let result = this.toString();
if(String(Number(result)) === result) return Number(result);
else return result;
}
let textNodes = [];
let oddsSum = 0;
let oddsArray = [];
for(let key of this.$children) {
let node = this[key];
if(!consumedNodes.has(node)) {
let odds = node.$odds; // remember, $odds is a getter and could be dynamic, so we musn't call it multiple times and expect the same result
// if it has odds of zero, leave it out - but don't consume it, because it hasn't been consumed (zero odds could be temporary, for example)
// EDIT: turns this doesn't really make sense for non-consumable lists (because you end up with problems like this: https://www.reddit.com/r/perchance/comments/co6msx/bug_list_name_being_included_in_output_of_list/)
// and it's not really that useful for cosumableLists either except to help people find bugs where their list ran out of non-zero-odds items. So I think
// due to the problems it causes with peoples' code it's not worth the trouble. So I'm commenting it out for now.
//if(odds === 0 && isConsumableList) continue;
if(odds < 0 || isNaN(odds)) {
__perchanceError(`The item on this line has the following odds: <code>${node.$oddsText}</code>. This has resulted in an odds value of <code>${odds}</code>, which is not valid. It should be a positive number.`, {declarationLineNumber:node.$declarationLineNumber, moduleName:node.$moduleName});
}
textNodes.push(node);
oddsSum += odds;
oddsArray.push(odds);
}
}
if(textNodes.length === 0) {
if(consumedNodes.size > 0) return `(no more items in the consumable '${this.$text}' list)`;
else {
let out = __evaluateText(this.$root, this.$parent, this.$text, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName}); // we need to return the text if no children, otherwise we can't do nice stuff like this: https://www.reddit.com/r/perchance/comments/6g1uqk/fixed_variables/dio2da4/
if(window.generatorLastEditTime > 1716337636385) { // May 22nd 2024 bugfix, ensures only generators edited after this time get it, to prevent breaking old generators that rely on the bug
out = __processEscapedCharacters(out);
}
return out;
}
// and we need to *evaluate* the text because otherwise we couldn't do `c = b.selectOne.selectOne` in this example: https://www.reddit.com/r/perchance/comments/6qqc4d/items_in_shorthand_lists_arent_always_stored/dl01lkv/
//else return `('${this.$text}' is not a list)`;
// don't throw error because it's okay for a node not to have children
//console.error(`Error trying to get child nodes of '${this.$text}' which was declared on line number ${this.$declarationLineNumber}.`);
// if(this.isConsumable) return `(no more items in the consumable '${this.$text}' list)`;
// else return `('${this.$text}' is not a list)`; // TODO***: return `this.$text` here since it's not consumable and has no children?
}
// choose random position in oddsSum range
let oddsSumStopPoint = oddsSum*Math.random();
let selectedTextNode = null;
let oddsSumForChoice = 0;
for(let i = 0; i < textNodes.length; i++) {
oddsSumForChoice += oddsArray[i];
if(oddsSumForChoice >= oddsSumStopPoint) {
selectedTextNode = textNodes[i];
break;
}
}
if(selectedTextNode === null) {
__perchanceError(`This shouldn't happen! This may be a problem with the Perchance engine. Please report this bug to <a href='https://lemmy.world/c/perchance'>lemmy.world/c/perchance</a>. A text node couldn't be picked during a random selection in a node's .toString() function. Here's some extra details for the bug report: this.$text=${this.$text}; this.$children=${this.$children.join(",")}; this.$declarationLineNumber=${this.$declarationLineNumber}`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
console.log(this, textNodes);
console.error("Returning first text node rather than random one to prevent crashing while debugging.");
return textNodes[0];
}
// if(this.isConsumable) {
// let text = selectedTextNode.$text;
// this.$children = this.$children.filter(c => c !== text);
// delete this[text];
// }
return selectedTextNode;
}
let __toStringMethod = function(opts={}) {
// NOTE: We can't call processEscapedCharacters on the text before returning it because this method gets called
// DURING the evaluateSquareBlock calls that are inside evaluateText, and so processEscapedCharacters would get
// called multiple times on the same text. We actually (sort of counterintuitively) want to keep all the backslashes
// in the input until just before the whole string is returned.
// I was previously doing this by calling processEscapedCharacters in the updateTemplatedNodes function, but the
// problem with that is that if you have something like onclick="outputEl.innerHTML = animal.description" it won't
// remove the backslashes from the animal.description text. I was previously solving that by using the MutationObserver
// stuff, but that still doesn't cut it, because when you grab the text from a Perchance node and manipulate it with JS,
// you obviously don't expect it to have the escape characters in it.
// --> WAIT. I haven't slept in way too long, but I have a crazy theory that we actually don't want to strip the backslashes
// when the text is "viewed" from JavaScript. JavaScript is the "engine" so it makes sense that we'd see the text in its
// most "raw" state - no processing at all. Oh, so I just need to make evaluateItem strip the backslashes??
// --> WAIT AGAIN: I don't think that makes sense. When we convert a node/item to a string, we expect it to just be a plain
// string. It's "out of perchance" at that point. It's just meant to be a plain old string. So I think the obvious thing
// to do here (regarding the above note about this being used in the evaluateText function - and wanting to preserve
// backslashes until right before the final string is returned from this function) is just to use a version of this method
// which specifically doesn't remove backslashes, right? Should this function just take an options object? Yep, I'm going
// with that.
// TODO***: use getOwnPropertyDescriptor to detect $output? since it could be defined and yet return undefined
// make sure you update other methods too
let $output = this.$output;
if($output !== undefined) {
$output = typeof $output === "number" ? $output.toString() : $output.toString({keepEscapes:opts.keepEscapes}); // only pass the keepEscapes object if it's not a number (because numbers expect a radix as the param of toString)
let result = __evaluateText(this.$root, this.$parent, $output, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
if(opts.keepEscapes) return result;
else return __processEscapedCharacters(result);
}
if(this.$nodeType === "value") {
if(typeof this.$value === "number") { return this.$value; }
else {
// if it's a consumableList and is plain inline notation like {1|2|3|4|...} with nothing before or after, then let's consume it
let text = this.$value;
if(this.$alreadyConsumedItems && text[0] === "{" && text[text.length-1] === "}") {
let items = __splitUpCurlyOrBlock(text.substr(1, text.length-2));
if(items) { // i.e. if it's a valid curly OR block
// remove already consumed items
items = items.filter(i => !this.$alreadyConsumedItems.has( __getTextOddsDetails(i).textWithoutOdds || i )); // "|| i" needed because getTextOddsDetails returns false if no odds details
if(items.length === 0) return `(inline consumable list '${this.$key}' has no more items)`;
// choose an item
let chosenItem = __chooseRandomTextByOdds(this.$root, this.$parent, items, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
// add the chosen item to $alreadyConsumedItems
this.$alreadyConsumedItems.add(chosenItem);
// return the chosen item
if(opts.keepEscapes) return chosenItem;
else return __processEscapedCharacters(chosenItem);
}
}
if(this.$alreadyConsumedItems) __perchanceError(`You tried to make this into a consumable list: <code>${__escapeHTMLSpecialChars(this.$key)} = ${__escapeHTMLSpecialChars(this.$value)}</code>, but for an inline item to be consumable, it must be of the format <code>a = {b|c|d}</code> and cannot have anything outside of the curly brackets (e.g. you could not make <code>a = {b|c|d}efg</code> consumable).`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
let result = __evaluateText(this.$root, this.$parent, this.$value, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
if(opts.keepEscapes) return result;
else return __processEscapedCharacters(result);
}
}
// toString treats childless nodes differently to ones that have children (i.e. items that are themselves lists)
// this sounds like a terrible idea at first, but I've put a bit of thought into it and it seems like the best option
// so that newbies don't have to understand how the engine works just to simply do: [f = flower.selectOne]. The
// alternative would be to make them do [f = flower.selectOne.itemName] which is ridiculous.
// If you think about it, this approach actually makes a bit of sense. If `this` node is itself a list, then that's
// an important fundamental difference, and so a difference in behaviour isn't really too unexpected.
// What we're basically saying is that leaves are treated differently to branches. The awesome part about this
// approach is that it alls us to do `[a = animal.selectOne] ... [a.genus]` - i.e. we can access the *properties*
// of the selected animal. [a=animal.outputText] doesn't allow us to do this.
if(this.$children.length === 0) {
let result = __evaluateText(this.$root, this.$parent, this.$text, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
if(opts.keepEscapes) return result;
else return __processEscapedCharacters(result);
}
if(this.$children.length > 0) {
let child = this.selectOne;
if(typeof child === 'string') { // e.g., if it is an error message
return child;
}
let text = child.$text;
let result = __evaluateText(this.$root, child.$parent, text, {declarationLineNumber:child.$declarationLineNumber, moduleName:child.$moduleName});
if(opts.keepEscapes) return result;
else return __processEscapedCharacters(result);
}
};
// let addItemMethod = function(key) {
// this.$children.push(key);
// this[key] = undefined;
// console.log("THIS NEEDS FIXING! (see TODO, below this line)")
// // TODO: fix this? what if they want to add a child of the added child? needs to be a *proper* node?
// // TODO: implement $addChildren and $removeChildren methods ($addChild and $removeChild should really just be aliases of these)
// };
// let removeItemMethod = function(key) {
// this.$children.filter(k => k !== key);
// delete this[key];
// // TODO: verify that this works and makes sense
// };
let __$oddsMethod = function() {
if(this.$oddsText == '1') return 1;
if( String(Number(this.$oddsText)) === this.$oddsText ) return Number(this.$oddsText);
// NOTE: must use $oddsText odds rather than this.$odds, because this is the definition of $odds! (it'd be recursive)
if(typeof this.$oddsText !== 'string') {
__perchanceError(`This shouldn't happen. This may be a problem with the Perchance engine. Please report this bug to <a href='https://lemmy.world/c/perchance'>lemmy.world/c/perchance</a>. The odds property of this node isn't a string. Here's some extra details for the bug report: this.$text=${__escapeHTMLSpecialChars(this.$text)}; this.$children=${__escapeHTMLSpecialChars(this.$children.join(","))}; this.$declarationLineNumber=${__escapeHTMLSpecialChars(this.$declarationLineNumber)}; this.$oddsText=${__escapeHTMLSpecialChars(this.$oddsText)}`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
return 1;
}
let evaluatedOdds = __oddsTextToNumber(this.$root, this.$parent, this.$oddsText, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
if(typeof evaluatedOdds === 'number') {
return evaluatedOdds;
} else {
// TODO: potentially convert strings to numbers using eval (if it is a number, obviously). The reason
// I'm not doing it yet is because I may do string->number conversions automatically across the whole system.
// i.e. in the evaluateSquareBlock function itself.
__perchanceError(`The '^' character is used to specify how likely an item is of being chosen during a random selection. This line appears to have a '^' character, but the text after that character (<b>${__escapeHTMLSpecialChars(this.$oddsText)}</b>) isn't a number, or didn't evaluate to a number. You're allowed to use square and curly bracket expressions to randomly/dynamically determine the odds, but these expressions must evaluate to a number. In this case your odds expression didn't result in a number.`, {declarationLineNumber:this.$declarationLineNumber, moduleName:this.$moduleName});
return 1;
}
};
// let shuffleMethod = function(numShuffles) {
//
// // TODO***: can't order object keys, so this method is useless on nodes (but useful for arrays and strings)
//
// if(numShuffles === undefined) {
//
// var array = this.$children;
// var currentIndex = array.length, temporaryValue, randomIndex;
// // While there remain elements to shuffle...
// while (0 !== currentIndex) {
// // Pick a remaining element...
// randomIndex = Math.floor(Math.random() * currentIndex);
// currentIndex -= 1;
// // And swap it with the current element.
// temporaryValue = array[currentIndex];
// array[currentIndex] = array[randomIndex];
// array[randomIndex] = temporaryValue;
// }
// array = array.map(c => this[c]); // map to actual node objects
// array.toString = function() { this.join(""); }
// return array;
//
// } else {
//
// let text = this.toString().split("");
// for(let i = 0; i < numShuffles; i++) {
// let i1 = Math.floor(text.length*Math.random());
// let i2 = Math.floor(text.length*Math.random());
// let c1 = text[i1];
// let c2 = text[i2];
// text[i1] = c2;
// text[i2] = c1;
// }
// return text.join("");
//
// }
//
// }
let __joinItemsMethod = function(str) {
let $output = this.$output;
if($output !== undefined) {
return $output.toString();
}
let arr = [];
for(let c of this.$children) {
arr.push(this[c].getName);
}
return arr.joinItems(str+"");
};
let __selectUniqueMethod = function(...a) {
return this.consumableList.selectMany(...a);
};
let __selectManyMethod = function(...a) {
let num;
if(a.length === 1) {
if(Array.isArray(a[0])) {
num = Number(a[0][Math.floor(Math.random()*a[0].length)]);
} else {
num = Number(a[0]);
}
} else if(a.length === 2) {
num = Number(a[0]) + Math.round(Math.random()*(a[1]-a[0]));
} else if(a.length > 2) {
num = Number(a[Math.floor(Math.random()*a.length)]);
}
if(isNaN(num) || typeof num !== 'number') {
if(a.length === 0) {
__perchanceError(`You didn't give the selectMany() function any inputs when you used it on the node with the text "${__escapeHTMLSpecialChars(this.$text || this)}". You need to give it at least one input so it knows how long the list/array should be.`, {moduleName:this.$moduleName});
} else {
__perchanceError(`There's a problem with the inputs that you provided to the "selectMany" function when you used it on the node with the text "${__escapeHTMLSpecialChars(this.$text || this)}". You have given it the following input${a.length == 1 ? "" : "s"}: <b>${__escapeHTMLSpecialChars(a.join("</b>, <b>"))}</b>. ${a.length == 1 ? "This" : "These"} input${a.length == 1 ? "" : "s"} should be numeric (i.e. ${a.length == 1 ? "it" : "they"} should be${a.length == 1 ? " a " : " "} number${a.length == 1 ? "" : "s"}).`, {moduleName:this.$moduleName});
}
return ["(syntax error)"];
}
let arr = [];
for(let i = 0; i < num; i++) {
arr.push(this.selectOne);
}
// overwrite default array behaviour (selects a random one)
// in the case where they don't specify a join() after the repeat:
arr.toString = function() { return this.join(""); };
return arr;
};
let __titleCaseMethod = function() {
return this.toString().split(' ').map((s) => (s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase())).join(' ');
};
let __sentenceCaseMethod = function() {
return this.toString().split(/([!?.]+)/g).map((s) => {
let i = s.search(/[a-zA-Z]/);
let a = s.slice(0, i);
let b = s.slice(i, i+1).toUpperCase();
let c = s.slice(i+1);
return a + b + c;
}).join("");
};
let __upperCaseMethod = function() {
//debugger;
return this.toString().toUpperCase();
};
let __lowerCaseMethod = function() {
return this.toString().toLowerCase();
};
!function(e,a){"function"==typeof require&&"object"==typeof exports&&"object"==typeof module?module.exports=a():"function"==typeof define&&define.amd?define(function(){return a()}):e.pluralize=a()}(this,function(){var e=[],a=[],i={},r={},s={};function o(e){return"string"==typeof e?new RegExp("^"+e+"$","i"):e}function t(e,a){return e===a?a:e===e.toLowerCase()?a.toLowerCase():e===e.toUpperCase()?a.toUpperCase():e[0]===e[0].toUpperCase()?a.charAt(0).toUpperCase()+a.substr(1).toLowerCase():a.toLowerCase()}function n(e,a){return e.replace(a[0],function(i,r){var s,o,n=(s=a[1],o=arguments,s.replace(/\$(\d{1,2})/g,function(e,a){return o[a]||""}));return t(""===i?e[r-1]:i,n)})}function u(e,a,r){if(!e.length||i.hasOwnProperty(e))return a;for(var s=r.length;s--;){var o=r[s];if(o[0].test(a))return n(a,o)}return a}function l(e,a,i){return function(r){var s=r.toLowerCase();return a.hasOwnProperty(s)?t(r,s):e.hasOwnProperty(s)?t(r,e[s]):u(s,r,i)}}function c(e,a,i,r){return function(r){var s=r.toLowerCase();return!!a.hasOwnProperty(s)||!e.hasOwnProperty(s)&&u(s,s,i)===s}}function h(e,a,i){return(i?a+" ":"")+(1===a?h.singular(e):h.plural(e))}return h.plural=l(s,r,e),h.isPlural=c(s,r,e),h.singular=l(r,s,a),h.isSingular=c(r,s,a),h.addPluralRule=function(a,i){e.push([o(a),i])},h.addSingularRule=function(e,i){a.push([o(e),i])},h.addUncountableRule=function(e){"string"!=typeof e?(h.addPluralRule(e,"$0"),h.addSingularRule(e,"$0")):i[e.toLowerCase()]=!0},h.addIrregularRule=function(e,a){a=a.toLowerCase(),e=e.toLowerCase(),s[e]=a,r[a]=e},[["I","we"],["me","us"],["he","they"],["she","they"],["them","them"],["myself","ourselves"],["yourself","yourselves"],["itself","themselves"],["herself","themselves"],["himself","themselves"],["themself","themselves"],["is","are"],["was","were"],["has","have"],["this","these"],["that","those"],["echo","echoes"],["dingo","dingoes"],["volcano","volcanoes"],["tornado","tornadoes"],["torpedo","torpedoes"],["genus","genera"],["viscus","viscera"],["stigma","stigmata"],["stoma","stomata"],["dogma","dogmata"],["lemma","lemmata"],["schema","schemata"],["anathema","anathemata"],["ox","oxen"],["axe","axes"],["die","dice"],["yes","yeses"],["foot","feet"],["eave","eaves"],["goose","geese"],["tooth","teeth"],["quiz","quizzes"],["human","humans"],["proof","proofs"],["carve","carves"],["valve","valves"],["looey","looies"],["thief","thieves"],["groove","grooves"],["pickaxe","pickaxes"],["passerby","passersby"]].forEach(function(e){return h.addIrregularRule(e[0],e[1])}),[[/s?$/i,"s"],[/[^\u0000-\u007F]$/i,"$0"],[/([^aeiou]ese)$/i,"$1"],[/(ax|test)is$/i,"$1es"],[/(alias|[^aou]us|t[lm]as|gas|ris)$/i,"$1es"],[/(e[mn]u)s?$/i,"$1s"],[/([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i,"$1"],[/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,"$1i"],[/(alumn|alg|vertebr)(?:a|ae)$/i,"$1ae"],[/(seraph|cherub)(?:im)?$/i,"$1im"],[/(her|at|gr)o$/i,"$1oes"],[/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i,"$1a"],[/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i,"$1a"],[/sis$/i,"ses"],[/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i,"$1$2ves"],[/([^aeiouy]|qu)y$/i,"$1ies"],[/([^ch][ieo][ln])ey$/i,"$1ies"],[/(x|ch|ss|sh|zz)$/i,"$1es"],[/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i,"$1ices"],[/\b((?:tit)?m|l)(?:ice|ouse)$/i,"$1ice"],[/(pe)(?:rson|ople)$/i,"$1ople"],[/(child)(?:ren)?$/i,"$1ren"],[/eaux$/i,"$0"],[/m[ae]n$/i,"men"],["thou","you"]].forEach(function(e){return h.addPluralRule(e[0],e[1])}),[[/s$/i,""],[/(ss)$/i,"$1"],[/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,"$1fe"],[/(ar|(?:wo|[ae])l|[eo][ao])ves$/i,"$1f"],[/ies$/i,"y"],[/(dg|ss|ois|lk|ok|wn|mb|th|ch|ec|oal|is|ck|ix|sser|ts|wb)ies$/i,"$1ie"],[/\b(l|(?:neck|cross|hog|aun)?t|coll|faer|food|gen|goon|group|hipp|junk|vegg|(?:pork)?p|charl|calor|cut)ies$/i,"$1ie"],[/\b(mon|smil)ies$/i,"$1ey"],[/\b((?:tit)?m|l)ice$/i,"$1ouse"],[/(seraph|cherub)im$/i,"$1"],[/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at
window.__pluralize396795627295786 = window.pluralize; // winning
delete window.pluralize;
let __pluralFormMethod = function() {
let input = this.toString();
return __pluralize396795627295786(input);
//let output = nlp(input).nouns().toPlural().out("text");
//return output === "" ? input : output; // don't add "s" by default because the lib should already do it, and sometimes plural is same as singular (e.g. furniture)
};
let __singularFormMethod = function() {
let input = this.toString();
return __pluralize396795627295786(input, 1);
// let output = nlp(input).nouns().toSingular().out("text");
// return output === "" ? input : output;
};
let nlpCompromiseAddedWords = {abolish:"Verb",abound:"Verb",abstract:"Verb",accent:"Verb",accomplish:"Verb",admonish:"Verb",alert:"Verb",ally:"Verb",appropriate:"Verb",astonish:"Verb",auction:"Verb",audition:"Verb",average:"Verb",awake:"Verb",award:"Verb",back:"Verb",backpedal:"Verb",banish:"Verb",bank:"Verb",bankrupt:"Verb",better:"Verb",bill:"Verb",blacklist:"Verb",bless:"Verb",blind:"Verb",bloody:"Verb",blossom:"Verb",board:"Verb",bob:"Verb",bombard:"Verb",bottle:"Verb",brave:"Verb",breakfast:"Verb",brief:"Verb",bus:"Verb",busy:"Verb",butcher:"Verb",butter:"Verb",cake:"Verb",calm:"Verb",captain:"Verb",care:"Verb",cash:"Verb",caution:"Verb",center:"Verb",cherish:"Verb",christen:"Verb",chronicle:"Verb",chuck:"Verb",circumvent:"Verb",clean:"Verb",clear:"Verb",club:"Verb",commission:"Verb",complete:"Verb",comply:"Verb",conceal:"Verb",condition:"Verb",content:"Verb",contrive:"Verb",cool:"Verb",correct:"Verb",corrupt:"Verb",critique:"Verb",cup:"Verb",dawn:"Verb",degenerate:"Verb",demolish:"Verb",deprive:"Verb",derive:"Verb",design:"Verb",diminish:"Verb",disable:"Verb",discard:"Verb",disregard:"Verb",don:"Verb",double:"Verb",down:"Verb",dry:"Verb",dull:"Verb",elaborate:"Verb",embellish:"Verb",empty:"Verb",engineer:"Verb",enlist:"Verb",equal:"Verb",erect:"Verb",exact:"Verb",faint:"Verb",fake:"Verb",fancy:"Verb",fine:"Verb",firm:"Verb",fish:"Verb",fit:"Verb",flatter:"Verb",flicker:"Verb",flourish:"Verb",fly:"Verb",ford:"Verb",forward:"Verb",foster:"Verb",foul:"Verb",free:"Verb",frequent:"Verb",fund:"Verb",furnish:"Verb",further:"Verb",garnish:"Verb",gossip:"Verb",grace:"Verb",grant:"Verb",ground:"Verb",group:"Verb",herald:"Verb",hoist:"Verb",hollow:"Verb",hope:"Verb",house:"Verb",hum:"Verb",humble:"Verb",ice:"Verb",impoverish:"Verb",indent:"Verb",index:"Verb",initial:"Verb",institute:"Verb",inventory:"Verb",knot:"Verb",last:"Verb",lavish:"Verb",lean:"Verb",light:"Verb",lobby:"Verb",long:"Verb",lower:"Verb",lunch:"Verb",mail:"Verb",man:"Verb",mark:"Verb",marshal:"Verb",mature:"Verb",mean:"Verb",mellow:"Verb",milk:"Verb",mimic:"Verb",mine:"Verb",model:"Verb",moderate:"Verb",motion:"Verb",mute:"Verb",narrow:"Verb",near:"Verb",nestle:"Verb",nick:"Verb",number:"Verb",nurse:"Verb",obscure:"Verb",oil:"Verb",open:"Verb",outlive:"Verb",overcrowd:"Verb",overdo:"Verb",overeat:"Verb",overflow:"Verb",overhaul:"Verb",overhear:"Verb",overheat:"Verb",overload:"Verb",overlook:"Verb",overpower:"Verb",overrule:"Verb",oversee:"Verb",overshadow:"Verb",oversleep:"Verb",overthrow:"Verb",overturn:"Verb",own:"Verb",panic:"Verb",part:"Verb",partition:"Verb",patent:"Verb",patrol:"Verb",pedal:"Verb",pepper:"Verb",perfect:"Verb",perish:"Verb",petition:"Verb",plant:"Verb",please:"Verb",ply:"Verb",police:"Verb",polish:"Verb",position:"Verb",post:"Verb",pound:"Verb",power:"Verb",press:"Verb",pressure:"Verb",prime:"Verb",pucker:"Verb",punish:"Verb",query:"Verb",quiz:"Verb",rain:"Verb",rally:"Verb",ration:"Verb",rear:"Verb",rebel:"Verb",rebound:"Verb",refurbish:"Verb",relish:"Verb",repeal:"Verb",replenish:"Verb",requisition:"Verb",research:"Verb",reserve:"Verb",rev:"Verb",revive:"Verb",right:"Verb",ring:"Verb",rival:"Verb",round:"Verb",salt:"Verb",sanction:"Verb",scent:"Verb",school:"Verb",secure:"Verb",separate:"Verb",service:"Verb",shut:"Verb",signal:"Verb",silhouette:"Verb",single:"Verb",slow:"Verb",smooth:"Verb",snicker:"Verb",snow:"Verb",sour:"Verb",space:"Verb",spare:"Verb",speed:"Verb",spike:"Verb",spiral:"Verb",spy:"Verb",square:"Verb",stable:"Verb",station:"Verb",steady:"Verb",steam:"Verb",stuff:"Verb",sue:"Verb",tame:"Verb",tan:"Verb",taper:"Verb",tarnish:"Verb",tender:"Verb",tense:"Verb",thin:"Verb",thunder:"Verb",till:"Verb",time:"Verb",top:"Verb",total:"Verb",trouble:"Verb",undercut:"Verb",underline:"Verb",undertake:"Verb",undervalue:"Verb",up:"Verb",upset:"Verb",utter:"Verb",vanish:"Verb",varnish:"Verb",void:"Verb",wade:"Verb",warm:"Verb",water:"Verb",weather:"Verb",wed:"Verb",welcome:"Verb",woo:"Verb",wound:"Verb"};
let __pastTenseMethod = function() {
let word = this.toString();
if(word === "hope" || word === "hopes" || word === "hoped" || word === "hoping") return "hoped";
if(word === "bill" || word === "bills" || word === "billed" || word === "billing") return "billed";
if(word === "dawn" || word === "dawns" || word === "dawned" || word === "dawning") return "dawned";
//return nlp(this.toString(), nlpCompromiseAddedWords).sentences().toPastTense().out("text")
let out = __nlpCompromise("They "+this.toString(), nlpCompromiseAddedWords).verbs().conjugate()[0];
return out ? out.PastTense : this.toString()+"ed";
};
let __futureTenseMethod = function() {
let word = this.toString();
if(word === "hope" || word === "hopes" || word === "hoped" || word === "hoping") return "will hope";
if(word === "bill" || word === "bills" || word === "billed" || word === "billing") return "will bill";
if(word === "dawn" || word === "dawns" || word === "dawned" || word === "dawning") return "will dawn";
//return nlp(this.toString(), nlpCompromiseAddedWords).sentences().toFutureTense().out("text")
let out = __nlpCompromise("They "+this.toString(), nlpCompromiseAddedWords).verbs().conjugate()[0];
return out ? out.FutureTense : this.toString()+"ed";
};
let __presentTenseMethod = function() {
let word = this.toString();
if(word === "hope" || word === "hopes" || word === "hoped" || word === "hoping") return "hopes";
if(word === "bill" || word === "bills" || word === "billed" || word === "billing") return "bills";
if(word === "dawn" || word === "dawns" || word === "dawned" || word === "dawning") return "dawns";
//return nlp(this.toString(), nlpCompromiseAddedWords).sentences().toPresentTense().out("text")
let out = __nlpCompromise("They "+this.toString(), nlpCompromiseAddedWords).verbs().conjugate()[0];
return out ? out.PresentTense : this.toString()+"ed";
};
let __negativeFormMethod = function() {
return __nlpCompromise(this.toString()).sentences().toNegative().out("text");
};
// let ordinalMethod = function() {
// return nlp.getOrdinal(this.toString());
// };
// let withArticleMethod = function() {
// return nlp.getWithArticle(this.toString());
// };</script> <script>function __evaluateSquareBlock(root, thisRef, expression, ctxInfo={}) {
// NOTE: if the `with` keyword ever gets actually deprecated (very, very unlikely - 2ality's
// article is a bit misleading I think), you can just craft up a function declaration with each
// of the root variables so they get passed in as the evaled function's arguments (simple!)
// new Function(param1, ..., paramN, funcBody) -> http://www.2ality.com/2014/01/eval.html
// only thing is: i need to make sure the getters are still triggered on the proxy when I grab the properties (why wouldn't they be?). hmm
// oh but also I don't want to trigger ALL of them (could be a couple of thousand params for every single square bloc executed...)
let originalExpression = expression;
expression = expression.trim();
//expression = prependReturnKeywordIfNeeded(expression);
//let evalJSCode = eval.bind(thisRef);
if(window.__throwErrorIfNonDirectListReferenceIsFoundDuringEvaluateSquareBlock) {
if(!__isValidJavaScriptIdentifier(expression)) {
throw new Error("this is a harmless error - it's just used to break out of the direct-node-linking process createPerchanceTree");
}
}
// **WITHIN a node**, we don't want references to `this` to result in "$output"
// Otherwise we're very limited in terms of how we can interact with "local" items (can't use selectOne, etc. EVEN IN `$output` (since it would reference itself no matter what))
// Note that we can't just temporarily set $output to undefined, because the square block that we're evaluating could reference another list which references THIS list and expects $output to work. We only want uses of `this` in this square block (and uses of `this` within chained methods like selectOne in this example: `[this.selectOne]` - hence the need to bind functions to this proxy)
// REF:kjhfw927f63ohwkgw82g
let proxiedThisRef = undefined;
if(Object.getOwnPropertyDescriptor(thisRef, "$output")) {
proxiedThisRef = new Proxy(thisRef, {
get: function(target, property) {
if(property === "$output") {
return undefined; // <-- we pretend that this list doesn't have an $output property so that node methods like toString don't use it.
} else if(property === "___$outputShouldBeHidden") {
return true; // this is a hack to tell the consumableList proxy to hide the $output property (since we can only bind a function to one of the proxies)
} else if(property === "___proxyTarget") {
return thisRef;
} else {
let desc = Object.getOwnPropertyDescriptor(target, property);
if(!desc) {
//perchanceError(`The '${property}' property doesn't exist within '${target.$text}'.`, ctxInfo.declarationLineNumber);
return undefined;
}
if(desc.get) {
return desc.get.bind(proxiedThisRef)(); // <-- need to bind to this proxy so that the $output property is hidden from all successively chained functions - e.g. this.consumableList.selectOne.toString()
} else if(desc.value && typeof desc.value === 'function') {
return desc.value.bind(proxiedThisRef);
} else {
return target[property];
}
}
}
});
}
try {
let tempHasHandlerHolder;
if(root.___isProxy) {
tempHasHandlerHolder = root.___proxyHandler.has;
root.___proxyHandler.has = window.__rootProxyHasHandler_Greedy.bind(root.___proxyHandler);
}
window.__currentEvaluateSquareBlockRoot = root; // this is for String.prototype.evaluateItem, etc. handlers because otherwise they have no way of accessing their root if they are from an *imported* generator.
let result = (function(){ return eval("with(root){"+expression+"}"); }).apply(proxiedThisRef || thisRef); // NOTE: we can't change this to `with(root.getSelf)` because we're using the proxy to make it so [thingThatDoesntExist] returns undefined rather than throwing a syntax error.
if(root.___isProxy) root.___proxyHandler.has = tempHasHandlerHolder; //.bind(root.___proxyHandler);
window.__currentEvaluateSquareBlockRoot = null;
if(result === undefined) {
__perchanceError(`The expression '[${__escapeHTMLSpecialChars(originalExpression)}]' returned nothing (<code>undefined</code>). You may be trying to reference a list, variable, or function that doesn't exist. Here are some common causes of this error: <ul><li>You tried to reference a list/variable that you haven't created. For example, if you wrote [animal], but there was no top-level list called "animal", then you would get this error. Note that names are <b>case-sensitive</b> - check your capitalization.</li><li>If you misspell a property/function name, then you'll get this error. For example "[noun.pluralFormm]" would produce this error because "pluralForm" has been misspelled.</li><li>If you try to access a property of an object that doesn't exist, or that evaluates to something which doesn't exist, then you'll get this error.</li></ul> These errors can sometimes be hard to debug, so after you've given it your best shot, please post a question over on the <a href="https://lemmy.world/c/perchance">perchance community</a> and a friendly community member will help you out :)`, ctxInfo);
return undefined;
} else {
if(typeof result === 'string' && String(Number(result)) === result) {
return Number(result);
} else {
return result;
}
}
} catch(e) {
if(window.__throwErrorIfNonDirectListReferenceIsFoundDuringEvaluateSquareBlock) {
throw new Error("ignore this");
}
if(e.message.toLowerCase().includes("call stack size")) {
__perchanceError(`There's a problem with the syntax of this expression: '[${__escapeHTMLSpecialChars(originalExpression)}]'. Here's the message that was returned when execution failed: <b>${__escapeHTMLSpecialChars(e.message.slice(0, 5000))}</b>. It may be that you've accidentally created an "infinite loop" by making a list reference itself, or something like that.`, ctxInfo);
} else if(e.message.toLowerCase().includes("token") && (originalExpression.includes("“") || originalExpression.includes("”"))) {
__perchanceError(`There's a problem with the syntax of this expression: '[${__escapeHTMLSpecialChars(originalExpression)}]'. Here's the message that was returned when execution failed: <b>${__escapeHTMLSpecialChars(e.message.slice(0, 5000))}</b>. It looks like you might be using "curly quotes" (like this: <code>“blah”</code>) instead of normal quotes (like this: <code>"blah"</code>)? Perchance requires that you use normal quotes for text inside square brackets (e.g. <code>[a = animal.selectOne,""]</code>).`, ctxInfo);
} else {
__perchanceError(`There's a problem with the syntax of this expression: '[${__escapeHTMLSpecialChars(originalExpression)}]'. Here's the message that was returned when execution failed: <b>${__escapeHTMLSpecialChars(e.message)}</b>. Here are some common mistakes: <ul><li>you tried to reference a variable/list that you haven't created</li><li>list names are case-sensitive: "[animal]" is different to [Animal], and you should use underscores in list names instead of spaces</li></ul>`, ctxInfo);
}
return "(syntax error)";
}
}
// function prependReturnKeywordIfNeeded(expression) {
// // if single line without return statement, add it
// if(/\n/.test(expression) || /(^|\s|;|\}|\{|\)|\()return\s/.test(expression)) {
// return expression;
// } else {
// return "return "+expression;
// }
// }
// function expressionHasSemiColon(text) {
//
// let escaped = false;
// let inJSExprString1 = false;
// let inJSExprString2 = false;
// let inJSExprString3 = false;
// let inRegExp = false;
//
// for(let i = 0; i < text.length; i++) {
// if(i !== 0) {
// if(text[i-1] !== "\\") { escaped = false; }
// }
// if(text[i] === "\\") { escaped = !escaped; }
//
// ////////////////////////////////////
// // skip regexp //
// ////////////////////////////////////
// if(!inRegExp && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && text[i] === "/") { inRegExp = true; continue; }
// if(inRegExp && text[i] !== "/") { continue; }
// if(inRegExp && !escaped && text[i] === "/") { inRegExp = false; continue; }
//
//
// ////////////////////////////////////
// // skip strings //
// ////////////////////////////////////
// if(!inJSExprString1 && !inRegExp && text[i] === "\"") { inJSExprString1 = true; continue; }
// if(inJSExprString1 && !escaped && text[i] !== "\"") { continue; }
// if(inJSExprString1 && !escaped && text[i] === "\"") { inJSExprString1 = false; continue; }
//
// if(!inJSExprString2 && !inRegExp && text[i] === "'") { inJSExprString2 = true; continue; }
// if(inJSExprString2 && !escaped && text[i] !== "'") { continue; }
// if(inJSExprString2 && !escaped && text[i] === "'") { inJSExprString2 = false; continue; }
//
// if(!inJSExprString3 && !inRegExp && text[i] === "`") { inJSExprString3 = true; continue; }
// if(inJSExprString3 && !escaped && text[i] !== "`") { continue; }
// if(inJSExprString3 && !escaped && text[i] === "`") { inJSExprString3 = false; continue; }
//
// if(!inJSExprString3 && !escaped && text[i] === "`") {
//
// if(!inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !inRegExp) {
//
// }
//
// }
//
// return false;
//
// }</script> <script>// Heads up: This code is extremely messy and hacked together. Many comments are outdated.
let NODE_ODDS_INDICATOR_CHARACTER = "^";
window.__rootProxyHasHandler; // <-- gets set after rootProxy creation
window.__rootProxyHasHandler_Greedy = function(target, prop) {
if(prop in window) {
if(prop in this.executeChain(target, this.capturedCalls)) {
return true;
} else {
return false; // don't be so greedy that we steal window's props even when `window` has it and `root` doesn't.
}
} else {
return true;
}
}
// window.globalProxyLoopCount = 0;
// function proxyLoopCountResetLoop() {
// window.globalProxyLoopCount = 0;
// setTimeout(proxyLoopCountResetLoop, 1000);
// }
// proxyLoopCountResetLoop();
function __createPerchanceTree(text, moduleName, backupModuleName /*<-- this is for the hacky node creation function*/, doNotPreprocess) {
const functionStartTime = Date.now();
/*
There are two main steps in creating the tree:
1. We split up the text into lines and collect all the data we need about each line (e.g. odds, line number, etc.) and tidy it up as necessary (e.g. .trim() it, or remove it completely if it's a comment). Then we group sets of lines that are below a function header into said function header. We create all the parent/child hierarchy links along the way.
2. We turn all that node data into a neat "perchance tree" which has the properties required to make the DSL work nicely, and we attach all the perchance methods to each node. Then we return the root node in this tree.
*/
// remove inline comments
let lines = text.replace(/\r/g,"").split("\n");
const commentRegex = /\/\//;
for(let i = 0; i < lines.length; i++) {
if(!commentRegex.test(lines[i])) continue; // <-- just an optimisation
lines[i] = __stripCommentFromLine(lines[i]);
}
// *trim* EMPTY lines (otherwise whitespace can throw indentation error in next step)
// we need to keep blank lines so line numbers are preserved. they're removed in a bit
lines = lines.map(text => /[^\s]/.test(text) ? text : text.trim());
// mixed spaces and tabs -> only tabs
for(let i = 0; i < lines.length; i++) {
let normed = __normaliseLineIndentsToTabs(lines[i]);
if(normed === false) {
__perchanceError(`There appears to be an indenting error on this line near this text: "${__escapeHTMLSpecialChars(lines[i].substr(0,30))}". Perchance lists should be indented with either one tab or two spaces (per nesting level). If you want to have a space before a list item, then you should start the item with "\\s" (backslash + "s") which will be converted into a space (you can read more about this in the <a href="/tutorial">tutorial</a>).`, {declarationLineNumber:i+1, moduleName});
return;
} else {
lines[i] = normed;
}
}
// make lines objects
for(let i = 0; i < lines.length; i++) {
lines[i] = {
text: lines[i],
indents: lines[i].search(/[^\t]/), //index of first non-tab
lineNumber:i+1, // plus one because line num start at 1 by convention
children:[],
parent: null,
// REMEMBER: if you add anything here you need to add it to root line bject and line objects created in inline declarations
}
lines[i].text = lines[i].text.trim(); //trim away indents and end whitespace
}
// remove blank lines, but keep track of them via a reference from the line above (which works fine for successive blank lines), so we can add them back for function bodies (since e.g. in multi-line string the blank lines matter)
lines = lines.filter((l, i) => {
if(l.text === "") {
if(lines[i-1]) lines[i-1].blankLineBelow = l;
return false;
} else {
return true;
}
});
// create parent-child hierarchy
let root = {
text:"<root>",
indents:-1,
lineNumber:-1,
children:[],
parent: null,
nodeType: "text",
odds: "1",
perchanceCode:"",
//expressionArray: ["<root>"],
};
let indentOwners = {0:root}; // maps # indents of current line to correct parent
for(let i = 0; i < lines.length; i++) {
let line = lines[i];
let indents = line.indents;
if(indentOwners[indents]) {
// this is child of indentOwners[indents]:
indentOwners[indents].children.push(line);
line.parent = indentOwners[indents];
// this becomes owner of next indent level:
indentOwners[indents+1] = line;
// and *no one* owns the indent level after that:
indentOwners[indents+2] = undefined;
} else {
__perchanceError(`There appears to be an indenting error on this line near this text: "${__escapeHTMLSpecialChars(line.text.substr(0,30))}". Perchance lists should be indented with either one tab or two spaces. If you want to have a space before a list item, then you should start the item with "\\s" (backslash + "s") which will be converted into a space (you can read more about this in the <a href="/tutorial">tutorial</a>).`, {declarationLineNumber:line.lineNumber, moduleName});
return;
}
}
// for each node, get the perchance text required to create it.
for(let i = 0; i < lines.length; i++) {
// for each line (ordered top to bottom)...
lines[i].perchanceCode = lines[i].text;
let j = 0;
let parent = lines[i].parent;
let ancestor = parent;
// append this line's text to *all* ancestors of this line (with appropriate indentation):
while(1) {
if(ancestor.parent === null) ancestor.perchanceCode += `${ancestor.perchanceCode ? "\n" : ""}` + lines[i].text; // handle case where ancestor === root node
else ancestor.perchanceCode += "\n" + " ".repeat(lines[i].indents - ancestor.indents) + lines[i].text;
// EDIT: we also need to add blank lines back in because they matter e.g. for multi-line strings in function bodies:
let blankLine = lines[i].blankLineBelow;
while(blankLine) {
ancestor.perchanceCode += "\n";
blankLine = blankLine.blankLineBelow;
}
ancestor = ancestor.parent;
if(!ancestor) break;
if(j++ > 100000) {
__perchanceError(`Some sort of looping/recursion error has occurred. Please make a post on the forum (lemmy.world/c/perchance) so I can take a look at this. Thanks!`, {moduleName});
return;
}
}
}
root.perchanceCode = text;
// check for variable names wrapped in square brackets when they're already within a square block:
const unnecessarySquareBracketsRegex = /([^\\]|^)\[([^\]]*?[^a-zA-Z0-9_$\]]|)\[[a-zA-Z0-9_$.]+?\][^\]]*?\]/;
// above regex explained:
// - we need the [^a-zA-Z_$\]] part (character right before inner square brackets must not be a variable-name character) because we don't want to show a warning for `[myList[key]]`, for example.
// - we need the dot in [a-zA-Z_$.]+ because we of course want to be able to detect and warn about something like `[a > 7 ? [list.subProp] : "blah"]`
// - this doesn't handle escaping properly (because the preceding backslash could itself be escaped), and doesn't properly handle the fact that square brackets could occur within strings/regex/etc. that are within the outer square block
const singleEqualsInDynamicOddsRegex = /\^\[[^\]]*[^=!<>]=[^=>][^\]]*\]\s*$/;
const singleEqualsInIfElseConditionRegex = /\[if\s*\([^\)]*[^=><!]=[^=>][^\)]*\)\s*\{/;
for(let i = 0; i < lines.length; i++) {
if(lines[i].text.length > 5000) continue;
if(lines[i].text.indexOf("[") === -1) continue; // <-- just an optimisation
// unnecessary square brackets:
if(lines[i].text.indexOf("\"") !== -1 || lines[i].text.indexOf("'") !== -1 || lines[i].text.indexOf("'") !== -1) {
if(lines[i].text.match(unnecessarySquareBracketsRegex)) { // don't use .test() --> https://stackoverflow.com/a/21373261/11950764
// try removing quoted parts to see if it's still a match (very hacky, but it's not a huuge deal if it causes us to miss some - it's better than false positives):
let newText = lines[i].text;
newText = newText.replace(/".*[^\\]?"/g, `""`);
newText = newText.replace(/`.*[^\\]?`/g, "``");
newText = newText.replace(/'.*[^\\]?'/g, `''`);
if(newText.match(unnecessarySquareBracketsRegex)) { // if it still matches, then it's ~likely a true positive.
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"square-brackets-wrapping-variable-name-within-square-block"});
}
}
} else {
if(lines[i].text.match(unnecessarySquareBracketsRegex)) {
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"square-brackets-wrapping-variable-name-within-square-block"});
}
}
if(lines[i].text.match(singleEqualsInIfElseConditionRegex)) {
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"single-equals-in-if-else-condition"});
}
// single equals in dynamic odds
if(lines[i].text.indexOf("^[") === -1) continue; // <-- just an optimisation
if(lines[i].text.match(singleEqualsInDynamicOddsRegex)) {
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"single-equals-in-dynamic-odds"});
}
}
// get node data for functions (pack all details into header node and remove children)
// note that we need to handle functions first because otherwise lines of js will be
// classed as other node types
for(let i = 0; i < lines.length; i++) {
let funcHeaderDetails = __getFunctionHeaderDetails(lines[i].text);
if(funcHeaderDetails) {
lines[i].nodeType = "function";
lines[i].functionName = funcHeaderDetails.name;
lines[i].functionArgs = funcHeaderDetails.args; // <-- array of arg names
lines[i].functionIsAsync = funcHeaderDetails.isAsync;
// recursively collect all children, grandchildren, etc into one flat array:
let children = lines[i].children;
let j = 0;
while(1) {
if(j >= children.length) { break; }
let c = children[j];
children.splice(j+1, 0, ...c.children); // must put them in correct pos
j++;
}
// remove children/grandchildren/... from lines array:
for(let c of children) {
lines = lines.filter(l => l !== c);
}
// concat children/grandchildren/... with newlines and add to functionBody property
lines[i].functionBody = children.map(l => {
let text = l.text;
// add black lines back in since they matter for e.g. multi-line strings:
let blankLine = l.blankLineBelow;
while(blankLine) {
text += "\n";
blankLine = blankLine.blankLineBelow;
}
return text;
}).join("\n");
// remove children
lines[i].children = [];
}
}
// get node data for inline functions
const functionNodeRegex = /=>/;
for(let i = 0; i < lines.length; i++) {
if(lines[i].nodeType) continue;
if(!functionNodeRegex.test(lines[i].text)) continue; // <-- just an optimisation
let inlineFuncDetails = __getInlineFunctionDetails(lines[i].text);
if(inlineFuncDetails) {
lines[i].nodeType = "function";
// inline functions shouldn't have children
if(lines[i].children.length > 0) {
__perchanceError(`It appears that you've tried to give an inline function children (child items). At this point, inline functions are not allowed to have children (though I could change this in the future if people need this feature). There's also the possibility that you accidentally created an inline function. Inline functions have the form: <b>functionName(input1, input2, ...) =&gt; functionBody</b>. If you'd like to use "=" in a literal sense, put a backslash character right before it like this: "\\=".`, {declarationLineNumber:lines[i].lineNumber, moduleName});
return;
}
lines[i].functionName = inlineFuncDetails.name;
lines[i].functionArgs = inlineFuncDetails.args; // <-- array of arg names
lines[i].functionBody = inlineFuncDetails.body;
lines[i].functionIsAsync = inlineFuncDetails.isAsync;
}
}
// TODO: process other node-types (list with args) here
const unescapedEqualsSignSpecialCharsRegex = /[!"\#%&'()*+,.\/:;<>?@\[\\\]^{|}~]/;
const unescapedEqualsSignHtmlElement = /<[a-zA-Z]+ /;
const unescapedEqualsSignUrl = /https?:\/\/[^ ]+\?/;
const primitiveNodeRegex = /=/;
let ignoreEqualsSignWarnings = false;
let reenableEqualsSignWarningsAtIndentLevel = null;
for(let i = 0; i < lines.length; i++) {
if(reenableEqualsSignWarningsAtIndentLevel !== null && lines[i].indents <= reenableEqualsSignWarningsAtIndentLevel) {
ignoreEqualsSignWarnings = false;
reenableEqualsSignWarningsAtIndentLevel = null;
}
if(lines[i].nodeType) continue;
if(!primitiveNodeRegex.test(lines[i].text)) continue; // <-- just an optimisation
let details = __getPrimitiveNodeDetails(lines[i].text);
if(details) {
// NOTE: We've discovered an inline/"primitive" thing, but here's the thing:
// If it's not a direct reference (i.e. value !== "[...]" or "{...}")
// but it DOES contain square/curly block(s), then we need to make it
// into a normal node with details.value as a child.
// See marker odj29hfi3j0d2kj0hx24f for the handling of $value nodes.
// The reason we need to do this is because if we had ``output = [prefix]-blah` (for example)
// then we couldn't call `output.selectMany(3)` on it if it was a $value node.
// Basically, the fact that I want to be able to create "direct references" with
// value nodes means a bunch of mucking around trying to make everything else work
// as expected.
// let blocks = splitTextAtAllBlocks(details.value);
// if(blocks.length > 1) {
// lines[i].nodeType = "text";
// lines[i].text = details.key;
// //lines[i].expressionArray = [details.key];
// let child = {
// text: details.value,
// indents: lines[i].indents+1,
// lineNumber:lines[i].lineNumber,
// children:[],
// parent: lines[i],
// nodeType:"text",
// odds:"1",
// //expressionArray: splitTextAtSquareBlocks(details.value),
// }
// lines[i].children.push(child);
// lines.splice(i+1, 0, child);
// } else {
lines[i].nodeType = "primitiveKeyValue";
lines[i].primitiveKey = details.key;
lines[i].primitiveValue = details.value;
if(!ignoreEqualsSignWarnings && typeof details.value === "string" && details.value.startsWith("[this.getRawListText")) {
ignoreEqualsSignWarnings = true;
reenableEqualsSignWarningsAtIndentLevel = lines[i].indents-1;
}
if(!ignoreEqualsSignWarnings) {
// NOTE: these if conditions have seemingly-redundant .includes(...) at the start as an optimization - may actually slow things down though, haven't tested
// warn if key contains space AND previous line was a normal item in the same list. So users can avoid this warning by putting all their properties (that have spaces in their key) at the top of the list.
if(details.key.trim().includes(" ") && lines[i-1] && lines[i-1].primitiveKey === undefined && lines[i-1].indents === lines[i].indents) {
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"unescaped-equals-sign"});
} else if(details.key.includes("<") && unescapedEqualsSignHtmlElement.test(details.key) && (details.value.trim().startsWith(`"`) || details.value.trim().startsWith(`'`))) { // also warn if it looks like e.g. <img src="..."
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"unescaped-equals-sign"});
} else if(details.key.includes("?") && unescapedEqualsSignUrl.test(details.key)) { // also warn if it looks like e.g. https://example.com?foo=bar
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"unescaped-equals-sign"});
}
}
/* else if(unescapedEqualsSignSpecialCharsRegex.test(details.key)) { // also warn if key has weird/special characters. This catches e.g. <img src="..." even if it's the first item in the list (which would prevent above case from catching it)
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"unescaped-equals-sign"});
}*/
//}
}
}
// extract all the node data (odds, expression array) for plain old text nodes
const oddsDetailsRegex = /\^/;
for(let i = 0; i < lines.length; i++) {
if(lines[i].nodeType) continue;
if(!oddsDetailsRegex.test(lines[i].text)) { // <-- just an optimisation
lines[i].odds = "1"; // as string because all the others are strings - system is built to take strings and interpret them as expresions/numbers
} else {
// extract and remove odds stuff first:
let oddsDetails = __getTextOddsDetails(lines[i].text);
let odds, textWithoutOdds;
if(oddsDetails) {
lines[i].nodeType = "text";
lines[i].odds = oddsDetails.odds;
lines[i].text = oddsDetails.textWithoutOdds.trim(); // <-- NOTE: spaces between end of text and odds notation are ignored in lists! thus we trim. (not in curly "or" blocks though)
} else {
lines[i].odds = "1"; // as string because all the others are strings - system is built to take strings and interpret them as expresions/numbers
}
}
lines[i].nodeType = "text";
// let arr = splitTextAtSquareBlocks(lines[i].text);
// if(arr) {
// lines[i].nodeType = "text";
// lines[i].expressionArray = arr;
// // if((lines[i].expressionArray.length > 1 || (lines[i].expressionArray.length === 1 && lines[i].expressionArray[0][0] === "[")) && lines[i].children.length > 0) {
// // console.error(`Error on line number ${lines[i].lineNumber}. Lines with random variables shouldn't have children.`);
// // return;
// // }
// }
// if(oddsDetails && !arr) {
// perchanceError(`It seems that you've got a '^' character where it shouldn't be. The '^' character is a special one that allows you to specify how likely a node is of being selected. This "odds notation" should not be present on functions or other non-text nodes because they aren't a part of the group which is randomly selected from (they are special children). If you want to use the odds character ("^") literally (i.e. not to declare likelihood), then you can put a backslash character before it like so: \\^`, lines[i].lineNumber);
// return;
// }
}
// // warn for unclassified lines and then set nodeType as "text"
// for(let i = 0; i < lines.length; i++) {
// if(!lines[i].nodeType) {
// perchanceError(`For some reason this line could not be classified properly. There may be something wrong with your syntax of this line, or it may be a bug with the Perchance engine. If you think that there's a bug, please report it here: <a href="http://reddit.com/r/perchance">reddit.com/r/perchance</a>. That would be very much appreciated! :)`, lines[i].lineNumber);
// lines[i].nodeType = "text";
// lines[i].expressionArray = [lines[i].text];
// }
// }
// next up, construct the actual perchance node tree.
// first create a node for each line
// then connect up parent/children
//
// we also the perchance methods including the .toString() function to each node.
// the toString function is where the magic happens. calling
// [myrootvar] will just return the myrootvar variable. when this
// gets joined to a string, `myrootvar.toString()` is called which
// recursively resolves it into an actual string value, taking into
// account odds and other expressions (in expressionArrays) that it
// finds along the way
// 1. convert lines to perchance nodes
let allNodes = [];
lines.unshift(root);
for(let i = 0; i < lines.length; i++) {
lines[i].node = {};
lines[i].node[Symbol.for("node data")] = { // TODO: why use symbol? just put it in a non-enumerable $data property? then fix up all references to it? TODO: can this be deleted after we're done creating tree?
parentNode:null,
childNodes:[],
root:root,
declarationLineNumber: lines[i].lineNumber,
odds: null,
};
// make default function properties writable (not needed because we use simple objects now)
// Object.defineProperty(lines[i].node, "name", {writable:true});
// Object.defineProperty(lines[i].node, "length", {writable:true});
// NOTE TO SELF: Object.defineProperty seems to cause some bad performance slow-downs, but I'm pretty sure
// we need to make these properties non-enumerable - at least, I think that was the original idea. Might be worth
// thinking about this at some point because the perf gains are definitely significant if you can somehow avoid defineProperty.
Object.defineProperty(lines[i].node, "$root", {value:root.node, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$declarationLineNumber", {value:lines[i].lineNumber, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$moduleName", {value:moduleName, writable:true, configurable:true});
Object.defineProperty(lines[i].node, Symbol.toPrimitive, {value:function(hint) {
return this.valueOf();
// if(hint === "default" || hint === "string") {
// return this.toString();
// } else {
// return this.valueOf();
// }
}, writable:true, configurable:true});
// add $valueChildren array which contains all children of type 'value' so we can get the "properties" of a node (for stuff like 'generateInstance' plugin or build-in feature)
Object.defineProperty(lines[i].node, "$valueChildren", {value:[], writable:true, configurable:true});
// this is used in isFunctionNode in the proxy stuff:
Object.defineProperty(lines[i].node, "$functionChildren", {value:[], writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$allKeys", {value:[], writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$allKeysSet", {value:new Set(), writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$perchanceCode", {value:lines[i].perchanceCode, writable:true, configurable:true});
__addNodeMethods(lines[i].node);
allNodes.push(lines[i].node);
}
// 2. fill in parent/child references:
for(let i = 0; i < lines.length; i++) {
let node = lines[i].node;
let nodeData = node[Symbol.for("node data")];
if(i === 0) { // i.e. if root - (since root has no parent)
nodeData.parentNode = null;
Object.defineProperty(node, "$parent", {value:null, writable:true, configurable:true});
} else {
nodeData.parentNode = lines[i].parent.node;
Object.defineProperty(node, "$parent", {value:lines[i].parent.node, writable:true, configurable:true}); // <-- give it a public reference to its parent that *isn't* enumerable
}
nodeData.childNodes = lines[i].children.map(line => line.node);
}
// 3. fill in the rest of the node details depending on the node type
let alreadyWarnedAboutDuplicateTopLevelListNameCount = 0;
for(let i = 0; i < lines.length; i++) {
let node = lines[i].node;
let nodeData = lines[i].node[Symbol.for("node data")];
// add normal text nodes
if(lines[i].nodeType === "text") {
nodeData.type = "text";
nodeData.odds = lines[i].odds;
//nodeData.expressionArray = lines[i].expressionArray;
Object.defineProperty(lines[i].node, "$nodeType", {value:"normal", writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$oddsText", {value:lines[i].odds, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$text", {value:lines[i].text, writable:true, configurable:true}); // <-- give it a public reference to its text that *isn't* enumerable
if(node.$parent) { // i.e. if not root
let key = node.$text;
// while(node.$parent[key] !== undefined) {// if there's already one called this, add {|} (hacky, but I think it's fine since they're just text nodes?)
while(node.$parent.$allKeysSet.has(key)) { // if there's already one called this, add {|} (hacky, but I think it's fine since they're just text nodes?)
key += "{|}";
if(alreadyWarnedAboutDuplicateTopLevelListNameCount < 3 && node.$parent.$parent === null) {
window.codeWarningsArray.push({lineNumber:lines[i].lineNumber, generatorName:moduleName, warningId:"duplicate-top-level-list-name"});
alreadyWarnedAboutDuplicateTopLevelListNameCount++;
}
}
node.$parent[key] = node; // not via defineProperty, therefore it's enumerable
nodeData.parentNode.$allKeys.push(key);
nodeData.parentNode.$allKeysSet.add(key);
}
}
// add user-defined functions
if(lines[i].nodeType === "function") {
nodeData.type = "function";
nodeData.functionName = lines[i].functionName;
nodeData.functionArgs = lines[i].functionArgs;
nodeData.functionBody = lines[i].functionBody;
nodeData.functionIsAsync = lines[i].functionIsAsync;
//let withStr = `window.root.$imports.includes("${moduleName}") ? window.root.$imports[window.root.$imports.indexOf("${moduleName}")] : window.root`;
let moduleRefStr = `window.moduleSpace["${moduleName || backupModuleName}"]`;
let finalFunctionBody = `
let tempHasHandlerRememberer927394 = ${moduleRefStr}.___proxyHandler.has;
${moduleRefStr}.___proxyHandler.has = window.__rootProxyHasHandler; /* .bind(${moduleRefStr}.___proxyHandler); */
${nodeData.functionArgs.map(a => __isValidJavaScriptIdentifier(a) ? `if(${a} && ${a}.getSelf) ${a} = ${a}.getSelf;` : '').join("")}
// make a proxy for the module so we can "exclude" this function's inputs from the "with" statement.
// this allows us to have inputs that are the same name as module globals and they won't be "overwritten" by those globals.
// I had to add the Number stuff because my numbers were acting funny after messing with their prototype: https://stackoverflow.com/questions/52580398/why-doesnt-new-arraynew-number3-produce-an-array-of-length-3 (edit: originally had just !isNaN(a), but I had to add the toPrecision and $nodeType type stuff because isNaN was executing valueOf on list objects/nodes, which obviously causes trouble)
let moduleRefProxy = new Proxy(${moduleRefStr}.___proxyTarget.obj, {
get: function(target, name) {
if(false) {} // <-- just to make the next line easier to write
${nodeData.functionArgs.map(a => { a = a.split("=")[0]; if(a.startsWith("...")) { a = a.slice(3); } return `else if (name === "${a}") { return typeof ${a} !== "undefined" && typeof ${a} !== "object" && !${a}.$nodeType && ${a}.toPrecision && ${a}.toExponential && !isNaN(${a}) ? Number(${a}) : ${a}; }` }).join("\n")}
/*else if (name === "b") { return 10; }*/
else { return target[name]; }
}
});
let returnValue;
with(moduleRefProxy) {
let root = ${moduleRefStr}.___proxyTarget.obj; // <-- because for some reason 'root' was referencing window.root (which is different to this modules root if this is an imported module) 💕
returnValue = (${nodeData.functionIsAsync ? "async " : ""}function(${nodeData.functionArgs.join(",")}) {
${nodeData.functionBody}
}).apply(this, [${nodeData.functionArgs.join(",")}]);
}
${moduleRefStr}.___proxyHandler.has = tempHasHandlerRememberer927394;
return returnValue;
`;
try {
if(nodeData.functionIsAsync) {
// Note: I don't think we actually need to make the "outer" function async, since it will always return a promise (made by the inner async function). But doing it anyway in case people try to write code like `myCoolFunction.constructor.name === "AsyncFunction"` or something.
let AsyncFunction = (async function () {}).constructor;
nodeData.functionRef = new AsyncFunction(nodeData.functionArgs.join(","), finalFunctionBody).bind(nodeData.parentNode);
} else {
nodeData.functionRef = new Function(nodeData.functionArgs.join(","), finalFunctionBody).bind(nodeData.parentNode);
}
} catch(e) {
console.error(e);
__perchanceError(`There appears to be a syntax error in the function called '${nodeData.functionName}'. Here's the error message: "${e.message}".`, {declarationLineNumber:lines[i].lineNumber, moduleName});
throw new Error(e);
}
//nodeData.parentNode[nodeData.functionName] = new Function(nodeData.functionArgs.join(","), nodeData.functionBody).bind(nodeData.parentNode);
Object.defineProperty(lines[i].node, "$nodeType", {value:"function", writable:true, configurable:true});
Object.defineProperty(nodeData.parentNode, nodeData.functionName, {value:nodeData.functionRef, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$text", {value:nodeData.functionName, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$function", {value:nodeData.functionRef, writable:true, configurable:true});
nodeData.parentNode.$functionChildren.push(lines[i].functionName);
nodeData.parentNode.$allKeys.push(lines[i].functionName);
nodeData.parentNode.$allKeysSet.add(lines[i].functionName);
}
if(lines[i].nodeType === "primitiveKeyValue") {
nodeData.type = "primitiveKeyValue";
nodeData.primitiveKey = lines[i].primitiveKey;
nodeData.primitiveValue = lines[i].primitiveValue;
// NOTE: it appears that then we write "[apple*10]" where apple={1|2|3}, JS implicitely calls toString on apple (even though it's a multiplication operation). so we don't need to worry about implementing valueOf
//node.$value = lines[i].primitiveValue;
//node.$key = lines[i].primitiveKey;
let value = lines[i].primitiveValue;
// if it's just plain text, process escaped characters (remove backslashes)
// EDIT: NO! That's a terrible idea. Why remove escaped characters?! If you do, `output = \[wassup\]` throws an error (obviously). Was there a reason I previously thought this was a good idea?
if(typeof value === 'string') {
let split = __splitTextAtAllBlocks(value, {declarationLineNumber:lines[i].lineNumber});
if(typeof value === 'string' && split.length === 1 && split[0][0] !== "[" && split[0][0] !== "{") { // only need to check start bracket because it it exists, then it is unescaped, then the last character *must* be a closing bracket (since splitted text array has length of 1) else the first bracket would be unclosed
//value = processEscapedCharacters(value);
Object.defineProperty(lines[i].node, "$isPlainPrimitive", {value:true, writable:true, configurable:true});
}
} else if(typeof value === "number" || typeof value === "boolean") {
Object.defineProperty(lines[i].node, "$isPlainPrimitive", {value:true, writable:true, configurable:true});
} else {
throw new Error("unknown primitive type??");
}
Object.defineProperty(lines[i].node, "$value", {value:value, writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$key", {value:lines[i].primitiveKey, writable:true, configurable:true});
//Object.defineProperty(nodeData.parentNode, lines[i].primitiveKey, {value:node, writable:true, configurable:true});
// Just to be helpful:
if(typeof nodeData.primitiveValue === "string" && nodeData.parentNode === nodeData.root.node && nodeData.primitiveKey === "$output" && nodeData.primitiveValue.substr(1, nodeData.primitiveValue.length-2).trim() === "this") {
__perchanceError("The generator called <code><a href='/"+moduleName+"#edit'>"+moduleName+"</a></code> has <code>$output</code> set to <code>[this]</code>. Perchance does this by default, so this line should be removed. That is, if you don't specify a top-level <code>$output</code> (the list you want to export) for your generator, then Perchance will export your <i>whole hierarchy</i>.", {declarationLineNumber:nodeData.declarationLineNumber, moduleName});
return;
}
Object.defineProperty(nodeData.parentNode, lines[i].primitiveKey, {
get:function() {
// MARKER:odj29hfi3j0d2kj0hx24f
if(node.$isPlainPrimitive) {
// When a user writes `thing.prop` we want them to be able to access the proper string (with escape chars removed), but the problem
// is that we use this same getter in the evaluateText process, and we definitely don't want to remove escapes during that process
// because it gets recursively evaluated so we'd be removing backslashes and evaluating blocks that should be evaluated. The toString
// method also had this problem, but I solved it by passing in a {keepEscapes:true} object when it is used in the evaluateText process,
// but since this is a getter, we can't pass in arguments, so I've used mega hax to do it (use global var). See the evaluateText function
// file for the actual location where this var is altered. It should always be false except for when it is used during the evaluateText process.
if(typeof node.$value === "string" && !window.__primitiveValueGetterKeepEscapesYolo) return __processEscapedCharacters(node.$value);
else return node.$value;
}
if(typeof node.$value === 'number') {
return node.$value;
} else if(typeof node.$value === 'string') {
// EDIT: (7th Aug 2019), realised due to [this post](https://www.reddit.com/r/perchance/comments/cmlfqh/imported_expression_returns_undefined/)
// that they can have something like:
// name = BATMAN
// $output
// n = ["[name]"]
// and the IMPORTING generator's `name` variable will be used instead, since we're only resolving the first "layer" of square brackets. If the
// getter returns a string, I'm pretty sure it should be a FULLY RESOLVED string (otherwise why resolve the first layer at all?). If you really do
// want to pass around unresolved strings, then you're probably a pro and you know how to use functions and stuff.
// example:
// imported: https://perchance.org/imported-2324234#edit
// importer: https://perchance.org/importing-93845793#edit
let ctxInfo = {declarationLineNumber:node.$declarationLineNumber, moduleName:node.$moduleName};
let splitted = __splitTextAtAllBlocks(node.$value, ctxInfo);
if(splitted.length === 1) {
if(splitted[0][0] === "[" /*&& splitted[0][splitted[0].length-1] === "]"*/) {
// we don't want to evaluateText on this because it'll be coerced into a string (we want to preserve "direct" references)
let expression = node.$value.substr(1,node.$value.length-2);
let result = __evaluateSquareBlock(node.$root, node.$parent, expression, ctxInfo);
while(1) {
if(typeof result === "string") {
// if it is a single square block, we need to preserve direct references to other nodes - if not we can just evaluateText
let newSplitted = __splitTextAtAllBlocks(result, ctxInfo);
if(newSplitted.length === 1 && newSplitted[0][0] === "[") {
let expression = result.substr(1,result.length-2);
result = __evaluateSquareBlock(node.$root, node.$parent, expression, ctxInfo);
} else if(newSplitted.length === 1 && /^\{import:[a-z0-9\-]+\}$/.test(newSplitted[0])) {
let moduleName = result.slice(8, -1);
let moduleRoot = window.moduleSpace[moduleName].getSelf;
return moduleRoot.hasOwnProperty("$output") ? moduleRoot.$output : moduleRoot;
} else {
// it's definitely not one lone square block, so it's definitely not a direct reference and thus we can just resolve it to plain text:
result = __evaluateText(node.$root, node.$parent, result, ctxInfo);
// if(window.generatorLastEditTime > 1716337636385) { // May 22nd 2024 bugfix, ensures only generators edited after this time get it, to prevent breaking old generators that rely on the bug
// out = __processEscapedCharacters(out);
// }
if(localStorage.test2974296 === "sfsdfdsdfss4") { // above bugfix causes issues with many generators, so this is for testing
result = __processEscapedCharacters(result);
}
break;
}
} else {
break;
}
}
return result;
} else if(splitted[0][0] === "{" /*&& splitted[0][splitted[0].length-1] === "}"*/) {
//let expression = node.$value.substr(1,node.$value.length-2);
//return __evaluateCurlyBlock(node.$root, node.$parent, expression, {declarationLineNumber:node.$declarationLineNumber});
//return "{"+expression+"}";
// ---
// return node; // for square blocks we evaluate it to get a direct reference, but there's no need for this with curly blocks (we let toString do the evalation)
// EDIT: (read above 7th Aug 2019 realisation - we need to do the evaluation ourselves so that it's evaluated within THIS generator's scope (in terms of `this` and global variables)):
// return evaluateText(node.$root, node.$parent, node.$value, {declarationLineNumber:node.$declarationLineNumber});
// EDIT AGAIN: NOPE! Luckily, this is a *node*, not a string, so the future toString will keep the correct context. If we evaluateText here, then you end up not being able to make
// a consumableList from an "inline" list like `icecreamflavor = {chocolate|vanilla|pistachio}` because `icecreamflavor` immediately resolves to one of the flavours as reported here: https://www.reddit.com/r/perchance/comments/co6msx/bug_list_name_being_included_in_output_of_list/ewgjzmm/
if(/^\{import:[a-z0-9\-]+\}$/.test(splitted[0])) {
let moduleName = splitted[0].slice(8, -1);
// return window.moduleSpace[moduleName].getSelf.$output;
let moduleRoot = window.moduleSpace[moduleName].getSelf;
return moduleRoot.hasOwnProperty("$output") ? moduleRoot.$output : moduleRoot;
} else {
return node;
}
} else {
// // plain text (no curly or square blocks) - LATER: why do we need to evaluate it then? not necessary?
//return evaluateText(node.$root, node.$parent, node.$value, {declarationLineNumber:node.$declarationLineNumber});
return __processEscapedCharacters(node.$value); // <-- WAIT - this will never occur?? The if(node.$isPlainPrimitive) above will catch all plain text nodes.
}
} else {
// we've found a non-direct reference that's not plain text.
// we need to return the *actual node* because otherwise if we have `output = [prefix]-blah` (for example)
// then we couldn't call `output.selectMany(3)` on it because `output` would be a plain string (like "pseudo-blah", for example).
// note that when used on $value nodes, selectOne will just return the node itself
return node;
// var n = node.createClone;
// n.$nodeType = "text";
// Object.defineProperty(lines[i].node, "$text", {value:n.$value, writable:true, configurable:true});
// Object.defineProperty(lines[i].node, "$oddsText", {value:"1", writable:true, configurable:true});
// delete n.$key;
// delete n.$value;
// return n;
}
} else if(typeof node.$value === 'boolean') {
return node.$value;
} else {
//console.error("Something went wrong? $value should be either a number, string, or boolean, right?");
return node.$value; // June 11th 2019: It's a direct reference to another node, or a reference to an objext or something. (I thought I was tyding direct references up somewhere else, but that wouldn't cut it anyway because they can change the value now that there's a setter (e.g. to a POJO or whatever))
}
},
set:function(v) { // June 11th 2019: Added this setter - why wasn't it set already? If it's not set then we can't change it and we get problems like this: https://www.reddit.com/r/perchance/comments/bz2smn/one_of_the_example_generator_is_broken_i_think_i/
node.$value = v;
return v;
},
configurable:true,
});
Object.defineProperty(lines[i].node, "$nodeType", {value:"value", writable:true, configurable:true});
Object.defineProperty(lines[i].node, "$text", {value:lines[i].primitiveKey, writable:true, configurable:true});
nodeData.parentNode.$valueChildren.push(lines[i].primitiveKey);
nodeData.parentNode.$allKeys.push(lines[i].primitiveKey);
nodeData.parentNode.$allKeysSet.add(lines[i].primitiveKey);
}
}
// add $children property to each node (an array of all of the keys of the node's non-primitive, non-function children that are leaf nodes (i.e. that are themselves childless))
// TODO: final confirmation: this should be just the keys, right? or would it be more useful to people as nodes? what are use cases of this property? [animal.$children] - actually yeah, it makes sense that it returns a key - if it returned an object then a random child of THAT object would be chosen!
for(let node of allNodes) {
let keys = Object.keys(node).filter(k => node[k].$nodeType === 'normal').sort((k1,k2) => node[k1].$declarationLineNumber-node[k2].$declarationLineNumber); // filter out primitives and sort by the actual order of the list
Object.defineProperty(node, "$children", {value:keys, writable:true, configurable:true});
}
// // error if any of the **direct children of root** aren't valid javascript identifiers.
// // this is necessary for two reasons:
// // 1. calling [else] will throw an error - but [thing.else] will not
// // 2. calling [my list] will throw an error - but [thing["my list"]] will not
// let warnLineNumbers = [];
// for(let key of root.node.$children) {
// if(!isValidJavaScriptIdentifier(key)) {
// warnLineNumbers.push( root.node[key].$declarationLineNumber );
// }
// }
// if(warnLineNumbers.length > 0) {
// if(warnLineNumbers.length > 10) {
// warnLineNumbers = warnLineNumbers.slice(1,9);
// warnLineNumbers.push("...");
// }
// console.warn(`Warning at line number${(warnLineNumbers.length > 1 ? "s" : "")+" "+warnLineNumbers.join(", ")}. All *top-level* items must only include letters, numbers and underscore characters (no spaces or special characters). They also must not start with a number. They also must not be any of these "reserved" names: break, do, instanceof, typeof, case, else, new, var, catch, finally, return, void, continue, for, switch, while, debugger, function, this, with, default, if, throw, delete, in, try class, enum, extends, super, const, export, import, implements, interface, let, package, private, protected, public, static, yield`);
// }
// TODO: add module name to all errors, or print module name when you catch errors
// TODO: add "need help?" link to all errors that links to community (reddit, probably)
// extract import statements:
let importedModuleNamesArray = [];
__ignorePerchanceErrors = true;
for(let line of lines) {
if(line.nodeType === 'function' || line.node.$isPlainPrimitive) continue;
let names = __collectImportedModuleNamesFromText(line.text);
importedModuleNamesArray.push(...names);
}
__ignorePerchanceErrors = false;
Object.defineProperty(root.node, "$imports", {value:[...new Set(importedModuleNamesArray)], writable:true, configurable:true});
Object.defineProperty(root.node, "$allNodes", {value:allNodes, writable:true, configurable:true});
// for(let node of allNodes) {
// if(node.$constructorFunction.prototype !== undefined) {debugger;}
// Object.defineProperties(node.$constructorFunction.prototype, Object.getOwnPropertyDescriptors(node));
// }
const allowedDollarVariables = ["$output", "$preprocess"];
for(let node of root.node.$allNodes) {
if(node.$nodeType === "value" && node.$text[0] === "$" && !allowedDollarVariables.includes(node.$text)) {
__perchanceError(`There's a problem with the '${moduleName}' generator. You've created a property with a name that starts with "$". This is highly discouraged because dollar-sign variables are reserved for special features such as $output and $preprocess. New features that are added to Perchance in the future may use other dollar-sign variable names and these may conflict with your variable-name choice and thus cause errors.`, {declarationLineNumber:node.$declarationLineNumber, moduleName});
continue;
}
let nodeName = node.$text;
// I use `moduleName !== null` here to hackily exclude the name check if this run of createPerchanceTree is due to a call of the createClone function (the only case where no module name is allowed). In this case it's fine for top-level nodes to have names to be invalid JS identifiers, because they aren't referencable as if they were global variables.
// Note that if this is a call to createClone, then `root` isn't the "real" root and is discarded once we've created the clone.
if(moduleName !== null && node.$parent === root.node && (!__isValidJavaScriptIdentifier(nodeName) || node.$text === "update")) {
__perchanceError(`There's a problem with the '${moduleName}' generator. You've created a top-level list called "<b>${__escapeHTMLSpecialChars(node.$text)}</b>", which is not allowed. Unfortunately top-level list names are subject to some strict rules:<ul><li>They must not contain any spaces</li><li>They must only contain letters (lower or upper case), numbers and underscores ("_")</li><li>They must not begin with a number</li><li>They must not be any of the following special "reserved" names: update, do, if, in, for, let, new, try, var, case, else, enum, eval, null, this, true, void, with, await, break, catch, class, const, false, super, throw, while, yield, delete, export, import, public, return, static, switch, typeof, default, extends, finally, package, private, continue, debugger, function, arguments, interface, protected, implements, instanceof</li></ul> Sorry for the inconvenience! These rules may seem strange, but they're needed to make the more advanced features of the perchance engine work.`, {declarationLineNumber:node.$declarationLineNumber, moduleName});
}
}
let fn = function(){};
fn.obj = root.node;
let rootProxy = new Proxy(fn, {
capturedCalls: [],
executionProperties: [
"toString",
"valueOf",
Symbol.hasInstance,
Symbol.isConcatSpreadable,
Symbol.iterator,
Symbol.match,
Symbol.prototype,
Symbol.replace,
Symbol.search,
Symbol.species,
Symbol.split,
Symbol.toPrimitive,
Symbol.toStringTag,
Symbol.unscopables,
Symbol.for,
Symbol.keyFor,
"selectMany",
"selectOne",
"selectAll",
"getSelf",
"getName",
"$imports",
"$allNodes",
"$odds",
"$text",
"$value",
"$key",
"$nodeType",
"$oddsText",
"$declarationLineNumber",
"$moduleName",
"$root",
"$parent",
"$children",
"isConsumable",
"consumableList",
"createClone",
"evaluateItem",
"joinItems",
//"listName",
// TODO: add new items here whenever you add any new special properties
],
organiseCalls: function(capturedCalls) {
// CAREFUL: do not move `apply`s away from their correct `get`
let textTransforms = [];
let nonTextTransforms = [];
let inApplyChain = false;
for(let i = 0; i < capturedCalls.length; i++) {
let call = capturedCalls[i];
if(call.type === "get" && textTransformNames.includes(call.name)) {
// if(i+1 < capturedCalls.length && capturedCalls[i+1].type === "apply") {
// perchanceError(`Tried to call a text transform '${call.name}' as a function/method. Text transforms like ${call.name}, pluralForm and titleCase are not functions. They shouldn't be followed by a pair brackets.`);
// }
textTransforms.push(call);
inApplyChain = true; // any `apply`s after this text transform must be moved with it
} else if(call.type === "apply" && inApplyChain) {
textTransforms.push(call);
} else {
nonTextTransforms.push(call);
inApplyChain = false;
}
}
let organisedCalls = [];
organisedCalls.push(...nonTextTransforms);
organisedCalls.push(...textTransforms);
// TODO***: should text transforms be applied before joinItems?
return organisedCalls;
},
executeChain: function(target, calls) {
let result = target.obj;
// let disable$output = false;
// if(calls.map(c=>c.name).includes("disable$output")) {
// debugger;
// disable$output = true;
// }
if(calls.length === 0) {
return target.obj;
}
// move text transforms to end (importantly, this method doesn't change the this.capturedCalls array - it returns a new one)
let capturedCalls = this.organiseCalls(calls);
let lastResult, secondLastResult;
for(let i = 0; i < capturedCalls.length; i++) {
let call = capturedCalls[i];
secondLastResult = lastResult; // needed for `apply` (since LAST result is the actual function, and not the object/thing that it's being being called from)
lastResult = result;
if(call.type === "get") {
result = result[call.name];
if(result === undefined && i === 0) {
result = window[call.name]; // try to fallback to global `window` if there's no list/function/property found - e.g. if they did [Math.min(hp, 100)] - "Math" is not a list
}
} else if(call.type === "apply") {
// in my case the `this` variable should be the thing that the method is being called from
// (this is done by default with getters)
result = result.apply(secondLastResult, call.args);
} else {
console.error("How could this be?");
}
if(result === undefined) {
// we DON'T return an error message because they may just want to check if the property exists.
return undefined;
}
// Remember that `result` could be a Proxy (imagine `$output = [noun.pluralForm]` - root.$output is a proxy with `noun` and `pluralForm` in the capturedCalls array (because $value nodes are evaluated when called (using a `get` handler))).
// If it IS a proxy, we want to append this proxy's capturedCalls array to the new one and execute it:
if(result.___isProxy) {
let leftOverCalls = capturedCalls.slice(i+1);
let allCalls = [...result.___proxyHandler.capturedCalls, ...leftOverCalls];
// TODO: just to confirm, we don't need to pass along secondLastResult do we? What if they called `apply` twice in a row? It's an edge case that might be annoying to debug later on
return this.executeChain(result.___proxyTarget, allCalls);
}
}
return result;
},
userDefinedNonNodePropertyIds: {},
getUserDefinedNonNodePropertyId: function(property, calls) {
// In case you're wondering what on earth is going on here:
// We need to get the proxy to return properies that the USER has set with scripts (in square blocks, functions, etc.).
// So we need to keep track of when they add their own properties to `root`.
// That way, when the proxy gets a `get` request for one of these "non-node" properties, it can return it straight up.
// Basically, any properties added to root AFTER compilation will always be returned in get requests (rather than returning a proxy).
// This will probably cause memory leaks if they're setting properties on root with dynamically generated names?
let seperator = "_{9287234632<<prop-id-call-seperator>>4632877592}_";
let id = "root";
for(let call of calls) {
if(call.type === "apply") {
return false;
}
id += seperator + call.name;
}
id += seperator + property;
return id;
},
isUserDefinedNonNodeProperty: function(property, calls) {
return this.userDefinedNonNodePropertyIds[ this.getUserDefinedNonNodePropertyId(property, calls) ] === true;
},
isFunctionNode: function(target, capturedCalls, property) {
// If `property` is referencing a function node of target.obj, then we want to
// return that function (not a proxy). That's why we need this function (we use it in the `get` handler)
// if there are `apply`s in the capturedCalls, return false, since a function node can only be a
// direct `get` chain from the root node (this is just an optimisation)
for(let c of capturedCalls) {
if(c.type === 'apply') return false;
}
let root = target.obj;
// NOTE: we don't need to worry about user-defined non-node properties, because we detect them in the `get` trap
// NOTE: remember, all nodes are functions, so be careful with your typeof logic (not used here anyway)
let ref = root;
for(let c of capturedCalls) {
if(ref.$children.includes(c.name)) ref = ref[c.name];
else return false;
}
if(ref.$functionChildren.includes(property)) return true; // the call path DOES reference a function node
else return false;
// OLD APPROACH: I have no idea why I was doing the reverse-lookup thing in this old approach. To get around proxy "looping" stuff???
// // Here's what we do:
// // 1. if there are `apply`s in the capturedCalls, return false, since a function node can only be a
// // direct `get` chain from the root node (this is just an optimisation)
// // 2. For each occurrance of `property` in root.$allNodes that is a `value` or `function` node, trace
// // back up the $parent chain, checking at each stage that it matches the capturedCalls `get` names
// // in reverse order. If you get back to root, run the capturedCalls on root and see if it's a function.
// // If it is, return true. Otherwise continue.
// // 3. If no matches, return false.
//
//
// for(let c of capturedCalls) {
// if(c.type === 'apply') return false;
// }
//
// let root = target.obj;
//
// for(let node of root.$allNodes) {
// if(node.$text !== property) continue;
// if(node.$nodeType !== 'value' && node.$nodeType !== 'function') continue;
//
// // so we know it's a value or function node, and we know the `property` matches its name.
// // now we need to check if the node's ancestry chain back to root matches the capturedCalls array
//
// let traceNode = node.$parent;
// for(let i = capturedCalls.length-1; i >= 0; i--) {
// if(capturedCalls[i].name !== traceNode.$text) break;
// traceNode = traceNode.$parent;
// }
//
// if(traceNode === root) { // found a full path/ancestry match
// // now we just need to check if it's a function:
// let ref = root;
// for(let c of capturedCalls) { ref = ref[c.name]; }
// // i replaced the below two commented out lines with the third to help solve some `get` loops happening in the proxy with this.isFunctionNode
// // ref = ref[property]; // the final `get`
// // if(typeof ref === 'function') return true; // found a function node!!!
// if(ref.$functionChildren.includes(property)) return true; // found a function node!!!
// }
//
// }
//
// return false;
},
isGetterProperty: function(target, property) {
let desc = Object.getOwnPropertyDescriptor(target.obj, property);
return desc && desc.get;
},
get: function(target, property, receiver) {
// commenting this out for now in favor of the perchance.org/my-generator#debugFreeze approach
// if(window.globalProxyLoopCount > 10000000) {
// throw new Error("It seems like you've got an infinite loop somewhere in your code. Are you referencing a list name within a child of that list, such that it creates an infinite loop?");
// }
// window.globalProxyLoopCount++;
if(property === "___isProxy") { return true; }
if(property === "___proxyTarget") { return target; }
if(property === "___proxyHandler") { return this; }
// if(property === "___addExecutionPath") {
// let last = this.capturedCalls[this.capturedCalls.length-1];
// this.capturedCalls.pop();
// this.userDefinedNonNodePropertyIds[ this.getUserDefinedNonNodePropertyId(last, this.capturedCalls) ] = true;
// }
//if(this.isGetterProperty(target, property) || this.executionProperties.includes(property) || this.isUserDefinedNonNodeProperty(property, this.capturedCalls) || this.isFunctionNode(target, this.capturedCalls, property)) {
if(true) { // testing out resolving all `get`s (QUESTION: was the only reason why we needed proxies for stuff like `pluralAnimalList = [animal.pluralForm]`? because that's really not needed anyway, and I don't think it's even possible in perchance's current state)
let result = this.executeChain(target, this.capturedCalls);
// if(typeof result === 'string' || typeof result === 'number')
// return result;
if(result === undefined) {
if(property === Symbol.toPrimitive) {
return function() { return undefined; }
} else {
__perchanceError(`the list/variable <code>${this.capturedCalls.map(c=>c.name).join("→")}</code> doesn't seem to exist in the <code>${target.obj.$moduleName}</code> generator.`, {moduleName:target.obj.$moduleName});
// if(property === Symbol.toPrimitive) return function() { return "(error)"; }
return "(error)";
}
}
let finalResult = result[property];
if(typeof finalResult === 'function' && !finalResult.___isProxy) {
finalResult = finalResult.bind(result);
}
return finalResult;
} else {
// need to return new proxy
let newHandler = {};
Object.assign(newHandler, this);
newHandler.capturedCalls = this.capturedCalls.slice(0);
newHandler.capturedCalls.push({type:"get", name:property});
let np = new Proxy(target, newHandler)
return np;
}
},
apply: function(target, thisArg, args) {
// // return a new proxy:
// let newHandler = {};
// Object.assign(newHandler, this);
// newHandler.capturedCalls = this.capturedCalls.slice(0);
// // add arguments to last call that was captured
// newHandler.capturedCalls.push({type:"apply", args});
// let np = new Proxy(target, newHandler);
// return np;
// i commented out the above code and added this because we always want functions to resolve immediately. otherwise we get this problem (see comment in bottom left): https://i.imgur.com/SY9MOt8.png -- as another example, people needed to write [a = dice("1d6").selectOne] instead of just [a = dice("1d6")]
// maybe I should make exceptions for joinItems and those other inbuilt functions?
this.capturedCalls.push({type:"apply", args});
return this.executeChain(target, this.capturedCalls);
},
set: function(target, property, value, receiver) {
// resolve it if it's a proxy (to prevent infinite loops when you do something like [abc=abc] (where abc is an already-defined top-level node), since that would set abc to its own proxy, so when you resolve it, it would resolve to the proxy, and the loop continues)
if(value && value.___isProxy) {
value = value.___proxyHandler.executeChain(value.___proxyTarget, value.___proxyHandler.capturedCalls);
}
let obj = this.executeChain(target, this.capturedCalls);
if(typeof obj !== 'object') {
__perchanceError(`You tried to set <code>${__escapeHTMLSpecialChars(this.capturedCalls.map(c=>c.name).join("."))}</code> to <code>${__escapeHTMLSpecialChars(value)}</code>, but it appears that <code>${__escapeHTMLSpecialChars(this.capturedCalls.map(c=>c.name).join("."))}</code> does not exist, or some other strange error has occurred.`);
return;
}
Object.defineProperty(obj, property, {value, writable:true, configurable:true}); // importantly we're executing defineProperty on an actual object, NOT a proxy
this.userDefinedNonNodePropertyIds[ this.getUserDefinedNonNodePropertyId(property, this.capturedCalls) ] = true;
return value;
},
isExtensible: function(target) { return Object.isExtensible(this.executeChain(target, this.capturedCalls)); },
preventExtensions: function(target) { return Object.preventExtensions(this.executeChain(target, this.capturedCalls)); },
getOwnPropertyDescriptor: function(target, prop) { return Object.getOwnPropertyDescriptor(this.executeChain(target, this.capturedCalls), prop); },
defineProperty: function(target, property, descriptor) { return Object.defineProperty(this.executeChain(target, this.capturedCalls), property, descriptor); },
has: function(target, prop) { return (prop in this.executeChain(target, this.capturedCalls)); },
deleteProperty: function(target, property) { return delete this.executeChain(target, this.capturedCalls)[property]; },
ownKeys: function(target) { return Reflect.ownKeys(this.executeChain(target, this.capturedCalls)); }
});
window.__rootProxyHasHandler = rootProxy.___proxyHandler.has;
// add this module to modulespace now otherwise our functions won't work for the direct-reference checks
// yolo
window.moduleSpace[moduleName] = rootProxy;
// EDIT: don't need this any more now that we've got isFunctionNode?
// make user-defined functions execute immediately upon being called (i.e. don't return a proxy)
// for(let i = 0; i < allNodes.length; i++) {
// if(allNodes[i].$nodeType === "function") {
// let node = allNodes[i];
// // we found a function, now lets build a call chain to it
// let calls = [];
// while(node.$parent) {
// calls.unshift(node.$parent.$text);
// node = node.$parent;
// }
// calls.shift(); // remove "<root>"
// let chain = [rootProxy];
// for(let c of calls) {
// chain.push(chain[chain.length-1][c]); // did it weirdly like this to prevent proxy setter being called
// }
// chain = chain[chain.length-1]; // the last element is the completed chain
// chain[allNodes[i].$text] = allNodes[i].$function; // EDIT: this calls the proxy setter and adds a "userDefinedNonNodeProperty" - was that the prupose?
// }
// }
// tie direct references to the actual nodes. e.g.:
// animal = [african_animal]
// $output = [person]
// so rather than [animal] and [$output] being $value nodes, they'll directly reference [aftican_animal] and [person].
// this is mainly needed to make $output behaviour sane.
for(let node of allNodes) {
if(node.$nodeType === "value" && !node.$isPlainPrimitive) {
let blocks = __splitTextAtAllBlocks(node.$value, {declarationLineNumber:node.$declarationLineNumber, moduleName:node.$moduleName});
if(blocks.length === 1 && blocks[0][0] === "[" && blocks[0][blocks[0].length-1] === "]") {
let expression = node.$value.substr(1,node.$value.length-2);
// only create link if they're DIRECTLY referencing another list:
if(__isValidJavaScriptIdentifier(expression)) {
// intermediate values can be non-direct references, and we don't want to e.g. execute `[a++]`, so we make sure that if __evaluateSquareBlock comes across something like that in a multi-step "indirection chain", it aborts
window.__throwErrorIfNonDirectListReferenceIsFoundDuringEvaluateSquareBlock = true; // epic
try {
__ignorePerchanceErrors = true; // it may fail because it could reference a function that hasn't been linked yet (i have no idea what's going on)
let refNode = __evaluateSquareBlock(root.node, node.$parent, expression, {declarationLineNumber:node.$declarationLineNumber, moduleName:node.$moduleName});
__ignorePerchanceErrors = false;
if(refNode && refNode.$nodeType /*|| Array.isArray(refNode)*/) { // did it not abort, and did it resolve to a node?
node.$parent[node.$text] = refNode;
}
} catch(e) {}
delete window.__throwErrorIfNonDirectListReferenceIsFoundDuringEvaluateSquareBlock;
}
// NOTE: curly IMPORT blocks get tied to their actual module after all imports are compiled (in iframe html)
// TODO**: does all this work by default now because of the new proxy approach? can I remove this part all together?
// TODO***: we need to pre-evaluate $output so it's a *direct* reference to the thing it references,
// but if we do that with other $value nodes, things can get messy:
//
// person
// name = [name.selectOne]
//
// `person.name` will always be same node!!
// if we call update it stays the same
// if we do [a = new person], the selection remains the same
// etc. - tonnes of problems
// here's why we WANT to be able to do this in the first place:
// person
// name = [name.selectOne]
// gender = [this.name.gender]
//
// note that we can already do this sort of thing:
//
// person
// age = {1-100}
// isAdult = [this.age > 18]
//
// potential solution: only pre-evaluate it if it's a direct reference to another node
// (just like $output always is).
}
}
}
// give each node an id:
for(let i = 0; i < allNodes.length; i++) {
Object.defineProperty(allNodes[i], "$id", {value:i, writable:true, configurable:true});
}
// fix up $root references so they point to proxy instead of actual node
// also remove node data symbol stuff
// TODO***: what about $parent references - should they be proxied too?
for(let i = 0; i < allNodes.length; i++) {
Object.defineProperty(allNodes[i], "$root", {value:rootProxy, writable:true, configurable:true});
delete allNodes[i][Symbol.for("node data")];
}
// at the moment, this is mainly so that other users don't get confused about strange syntax - they can easily see there's a preprocessor,
// but in the future I may speed up preprocessing by just grabbing the function from the top, compiling that, and then running the code through it.
// (instead of compiling the whole thing just to get the $preprocess function, as is happening now.) EDIT: oh, don't forget they can *import* preprocessor too!
if(root.node.$preprocess && root.node.$allNodes[1].$text !== "$preprocess") {
__perchanceError(`The special <code>$preprocess</code> function must be placed above all your other lists - at the very top of the Perchance code editor.`, {moduleName});
return;
}
if(root.node.$preprocess && !doNotPreprocess) {
let preprocess;
if(typeof root.node.$preprocess === 'function') {
preprocess = root.node.$preprocess;
} else {
if(!root.node.$preprocess.$value && root.node.$preprocess.$allKeys.includes("$preprocess")) return __perchanceError(`Looks like you've imported the '${root.node.$preprocess.$moduleName}' generator for use as a preprocessor, but the preprocessor should be defined as the <code>$output</code> in '${root.node.$preprocess.$moduleName}', rather than as <code>$preprocess</code>.`, {moduleName});
if(!root.node.$preprocess.$value.startsWith("{import:")) return __perchanceError(`The special <code>$preprocess</code> property must be defined as a function, or an imported function.`, {moduleName});
let preprocessorName = root.node.$preprocess.$value.slice(8, -1);
let module = window.moduleSpace[preprocessorName];
if(!module) {
__perchanceError(`The preprocessor '${preprocessorName}' could not be found?`, {moduleName});
return;
}
if(!module.$output || !module.$output.$nodeType === "function") return __perchanceError(`The '${preprocessorName}' was imported into '${moduleName}' as a preprocessor function, but the <code>$output</code> of '${preprocessorName}' doesn't exist or is not a function?`, {moduleName});
preprocess = module.$output;
}
let newInputText = preprocess(root.perchanceCode);
return __createPerchanceTree(newInputText, moduleName, backupModuleName, true);
}
console.log(`${moduleName} init: ${Date.now()-functionStartTime}ms`);
return rootProxy;
//return root.node;
// notes:
// TODO: remove todos that are already done...
// TODO: primitive declarations using "=" and update toString so it ignores primitive-valued properties
// TODO: in toString: add {a|b|c} notation handling (recusive)
// TODO: add array handling: we just treat ["a", "b^2", "c"] like {a|b^2|c} when Array.toString() is called (use the same back-end function)
// TODO: if we across duplicate sibling nodes, what do we do? just make the latter overwrite the prior? add the odds together?
}
// this is very hacky...
function __duplicatePerchanceNode(originalNode) {
originalNode = originalNode.getSelf;
// NOTE: node will be unreferencable from parent (since the node to be duplicated already holds that key).
// I think this is fine? They can always add it to the parent if some weird situation required that.
let root = __createPerchanceTree(originalNode.$perchanceCode, null, originalNode.$moduleName);
root = root.getSelf; // get target object, not proxy
for(let node of root.$allNodes) {
if(node === root) continue;
node.$id = originalNode.$root.getSelf.$allNodes.length;
originalNode.$root.getSelf.$allNodes.push(node);
node.$root = originalNode.$root.getSelf;
node.$moduleName = originalNode.$moduleName;
// I think this is causing some problems when it tries to get a node of a "directly linked" property:
//node.$declarationLineNumber = getCorrespondingOriginalNode(node).$declarationLineNumber;
}
// function getCorrespondingOriginalNode(clonedNode) {
// let propertyChain = [];
// let n = clonedNode;
// while(n.$parent) { // get chain up to cloned root
// propertyChain.unshift(n.$text);
// n = n.$parent;
// }
// n = originalNode.$parent.getSelf
// while(propertyChain.length) { // evaluate chain from original node's parent
// n = n[propertyChain[0]];
// propertyChain.shift();
// }
// return n;
// }
let node = root[originalNode.$text];
node.$parent = originalNode.$parent.getSelf;
return node;
}
// This function is PUBLIC - i.e. available for people to use in their generators
function createPerchanceTree(text) {
let root = __createPerchanceTree(text, null, "<null>"); // <null> is a special value for nodes that technically don't belong to any module
root = root.getSelf; // get target object, not proxy
return root;
}
function __escapeHTMLSpecialChars(unsafe) {
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function __collectImportedModuleNamesFromText(text) {
let blocks = __splitTextAtAllBlocks(text);
let moduleNames = [];
for(let b of blocks) {
if(b[0] === "{") {
let match = /^\{import:([a-z0-9\-]+)\}$/.exec(b);
if(match && match.length > 1) {
moduleNames.push( /^\{import:([a-z0-9\-]+)\}$/.exec(b)[1] );
} else {
moduleNames.push( ...__collectImportedModuleNamesFromText( b.substr(1, b.length-2) ) );
}
}
}
return [...new Set(moduleNames)];
}
// function evaluateTextAsNumber(rootNode, thisRef, text, ctxInfo) {
// // NOTE: This function is NOT for evaluating odds expressions!
// // NOTE: this only returns the number if it can be "exactly" converted
// // into a number - it doesn't evaluate the string's mathematical operators
// // or anything like that. It only returns if text === String(Number(text))
// // because that mean indicates that the deliberately typed a number, and even if
// // they didn't, the toString conversion that occurs when this number is output
// // will give **exacttly the same result anyway**.
// let value = __evaluateText(rootNode, thisRef, text, ctxInfo);
// if(value === String(Number(value))) {
// return Number(value);
// } else {
// return false;
// }
// }
function __evaluateText(rootNode, thisRef, text, ctxInfo, previousEvaluationText) {
// IMPORTANT: Some curly functions may "return themselves" i.e. {a} may return "{a}" rather than "a" or "an" - this
// behaviour is allowable because evaluateText is called recursively until no unescaped curly/square blocks
// exist. The reason curly functions may do this is because parts of their arguments (the two strings either side of
// "{a}" in the case of the "{a}" function) may not have been evaluated yet, and it may rely on those parts having
// been evaluated. e.g. in the text "{a} [animal] is over there", {a} must wait until [animal] has been evaluated.
// ALSO NOTE: previousEvaluationText is needed because (for example) if we evaluate "created {a}", there's nothing after {a} for
// us to detemrine whether we should use "a" or "an". But "created {a}" is still valid because they could be using it
// as part of a larger sentence. If `text` evaluates to `previousEvaluationText`, then we just return it rather than
// recuring infinitely.
// ALSO NOTE: I had to change this so that instead of [passing through once and then checking if there are any unevaluated blocks that would
// require another pass-over], it does the recursion *within* the loop - otherwise things get executed out of order (e.g. assignment of
// variables and stuff). The user expects their variable assignments to happen in the order that the final output text is printed in.
// Edit: Oh, but I still need to recurse at the end, because some curly blocks like {a} may only be executable once all the blocks are
// joined together into one string.
// ALSO NOTE: This function DOESN'T call processEscapedCharacters on the output. That must be called on the result.
var bracketRegex = /[\[\]{}]/; // note: this is used further down too
if(!bracketRegex.test(text)) return text; // if it has no brackets, nothing needs evaluating
var blocks = __splitTextAtAllBlocks(text, ctxInfo);
// iterate through and evaluate each block
var evaluatedBlocks = [];
var block, leftBlocks, rightBlocks;
for(let i = 0; i < blocks.length; i++) {
block = blocks[i];
leftBlocks = blocks.slice(0,i);
rightBlocks = blocks.slice(i+1);
if(block[0] === "{") {
let input = block.substr(1,block.length-2);
let result = __evaluateCurlyBlock(rootNode, thisRef, input, leftBlocks, rightBlocks, ctxInfo);
if(result && (typeof result === "object" || typeof result === "function") && result.$nodeType) {
// This could be a Perchance node due to import blocks. See note below the `__evaluateSquareBlock` call below for explanation.
result = result.toString({keepEscapes:true});
} else {
result = result + "";
}
// the first few conditions here are just performance optimisations:
if(result !== block && bracketRegex.test(result) && (result[0] === "[" || result[0] === "{" || __splitTextAtAllBlocks(result).length > 1)) {
result = __evaluateText(rootNode, thisRef, result, ctxInfo, result);
}
evaluatedBlocks.push(result);
} else if(block[0] === "[") {
let input = block.substr(1,block.length-2);
window.__primitiveValueGetterKeepEscapesYolo = true; // see MARKER:odj29hfi3j0d2kj0hx24f for explanation
let result = __evaluateSquareBlock(rootNode, thisRef, input, ctxInfo);
window.__primitiveValueGetterKeepEscapesYolo = false;
if(result && (typeof result === "object" || typeof result === "function") && result.$nodeType) {
// If it's a Perchance node, we keep the escape characters, since if we remove them here, then we would unescape square/curly character (for example) and
// then they'd get executed (in the code that follows this block) even though that's obviously not what the user wants. See the comments at the top of the toString
// function for more context.
result = result.toString({keepEscapes:true});
} else {
result = result + "";
}
// the first few conditions here are just performance optimisations:
if(result !== block && bracketRegex.test(result) && (result[0] === "[" || result[0] === "{" || __splitTextAtAllBlocks(result).length > 1)) {
result = __evaluateText(rootNode, thisRef, result, ctxInfo, result);
}
evaluatedBlocks.push(result);
} else {
evaluatedBlocks.push(block);
}
}
if(previousEvaluationText === evaluatedBlocks.join("")) return previousEvaluationText; // see note at top of this function for explanation
// if there's anything but plain text (i.e. if there's any curly/square blocks), recurse
var b, completelyEvaluated = true;
for(let i = 0; i < evaluatedBlocks.length; i++) {
b = evaluatedBlocks[i];
if(!bracketRegex.test(b)) continue;
//if(b === undefined) { b = evaluatedBlocks[i] = "(not found)"; }
if(b[0] === "[" || b[0] === "{" || __splitTextAtAllBlocks(b).length > 1) {
completelyEvaluated = false;
break;
}
}
if(!completelyEvaluated) {
return __evaluateText(rootNode, thisRef, evaluatedBlocks.join(""), ctxInfo, evaluatedBlocks.join(""));
} else {
return evaluatedBlocks.join("");
}
}
function __processEscapedCharacters(text) {
//TODO***: when they have an item like `\ hello \` and they repeat it like [item][item][item]... you end up with `hello \ hello \ hello \ ...` because the two backslashes join up.
// I think this is pretty easy: if there's an unescaped backslash on the end, remove it when you create the node.
// If there's one at the start and it's just escaping a space, then remove that backslash too.
// TODO***: allow them to escape forward slashes?
// TODO***: empty character "\e"?
// look for input characters that follow an unescaped backslash
// and replace them with the output character and remove the backslash
let escapableCharacters = [
{input:"=", output:"="},
{input:"{", output:"{"},
{input:"}", output:"}"},
{input:"[", output:"["},
{input:"]", output:"]"},
{input:"^", output:"^"},
{input:"|", output:"|"},
{input:"n", output:"\n"},
{input:"t", output:"\t"},
{input:"s", output:" "},
];
let escaped = false;
for(let i = 0; i < text.length; i++) {
if(i !== 0) {
if(text[i-1] !== "\\") {
escaped = false;
}
}
if(text[i] === "\\") {
if(escaped) { // handle the special case of the backslash character:
escaped = false;
text = text.substr(0, i-1) + "\\" + text.substr(i+1);
i--;
} else {
if(i === text.length-1) {
return text.substr(0, text.length-1); // if last character is an unescaped backslash, return text without it
} else {
escaped = true;
}
}
continue;
}
if(escaped) {
for(let e of escapableCharacters) {
if(text[i] === e.input) {
text = text.substr(0, i-1) + e.output + text.substr(i+1);
i--;
break;
}
}
}
}
return text;
}
function __evaluateCurlyBlock(rootNode, thisRef, block, leftBlocks, rightBlocks, ctxInfo) {
// `block` is a string of the *contents* of the curly block
for(let fn of __curlyFunctions) {
// note that recursion can occur here since some curly functions may neet to
// resolve their arguments to plain text. So they'll call evaluatedText, which
// will call this function (if curly brackets are found), and so the loop loops.
let evaluatedBlock = fn(rootNode, thisRef, block, leftBlocks, rightBlocks, ctxInfo);
if(evaluatedBlock !== false) {
if(typeof evaluatedBlock === 'string' && String(Number(evaluatedBlock)) === evaluatedBlock) {
return Number(evaluatedBlock);
} else {
return evaluatedBlock;
}
}
}
if(block === "s") {
__perchanceError(`Your curly block "<code>{${__escapeHTMLSpecialChars(block)}}</code>" doesn't appear to have the correct syntax. It is meant to be used for something like <code>I have {1-3} [fruit]{s}</code>. Notice that there needs to be a number preceding the <code>[fruit]</code> block. If you just want the plural of <code>[fruit]</code> then you can use <code>[fruit.pluralForm]</code>.`, {declarationLineNumber:ctxInfo.declarationLineNumber, moduleName:ctxInfo.moduleName});
} else {
__perchanceError(`Your curly block "<code>{${__escapeHTMLSpecialChars(block)}}</code>" doesn't appear to have the correct syntax. Curly brackets (these ones: {}) are special characters in Perchance. They're used to do all sorts of fancy stuff which you can learn about in the <a href="/tutorial">tutorial</a>. If you didn't intend to use a special function, and instead just wanted to actually use curly brackets as literal characters, then you can put a backslash character before them like so: \\{...\\}`, {declarationLineNumber:ctxInfo.declarationLineNumber, moduleName:ctxInfo.moduleName});
}
return "(invalid curly block)";
}
// function evaluateCurlylessBlock(rootNode, text) {
// // TODO: allow passing in the declarationLineNumber and other details to this function?
// let arr = splitTextAtSquareBlocks(text);
// let resultText = "";
// for(let e of arr) {
// if(e[0] === "[") {
// let jsExpr = e.substr(1, e.length-2);
// resultText += evaluateSquareBlock(rootNode, rootNode, jsExpr, {declarationLineNumber:null});
// } else {
// resultText += e;
// }
// }
// return resultText;
// }
function __getTextOddsDetails(text, ctxInfo={}) {
// JUST returns text on either side of unescaped "^" that is in a TEXT block, with some minor validation (just to help users debug)
let blocks = __splitTextAtAllBlocks(text, ctxInfo);
let blockI = 0;
let charI = 0;
let overallI = 0;
let currentBlock = blocks[0];
let inText = null;
function inTextBlock(checkI) {
// NOTE: we hold the state outside of the function so that we can start from where we left off
if(overallI > checkI) {
// start again if previous check state is past the current one
blockI = 0;
charI = 0;
overallI = 0;
currentBlock = blocks[0];
inText = null;
}
while(true) {
if(blockI > blocks.length-1) {
console.error("The checkI value (index to be checked to see if it's within a TEXT block) seems to be greater than the length of the whole text.");
return false;
}
// skip block if checkI doesn't occur within it
if(charI === 0) {
if(overallI + blocks[blockI].length-1 < checkI) {
overallI += blocks[blockI].length; //don't subtract 1 because that would put overallI at last character of current block - we want it to be at the first character of nect block
charI = 0; // not necessary since it already is zero, but just for expliciteness
blockI++; // only increase block index after adding block size to overallI
continue;
}
}
if(charI === 0) {
if(blocks[blockI][0] === "[" || blocks[blockI][0] === "{") {
inText = false;
} else {
inText = true;
}
}
if(overallI === checkI) {
return inText;
}
if(charI === blocks[blockI].length-1) { // if we've reached the end of a block
overallI++;
blockI++;
charI = 0;
} else {
charI++;
overallI++;
}
}
}
//text = text.trim(); // <-- I commented this out because inline OR notation like { a ^1| b ^2} would return "a " or "b " instead or " a " or " b "
let escaped = false;
for(let i = 0; i < text.length; i++) {
if(i !== 0) {
if(text[i-1] !== "\\") { escaped = false; }
}
if(text[i] === "\\") { escaped = !escaped; }
if(!escaped && text[i] === NODE_ODDS_INDICATOR_CHARACTER && inTextBlock(i)) {
let odds = text.substr(i+1).trim();
// tiny bit of validation, just to help out users, but definitely not an full validation (which would be basically impossible to do, since we'd need to simulate every possible combination of potentially huge programs)
if(!/[!0-9\[\{]/.test(odds[0])) {
__perchanceError(`There is a problem with this odds declaration: "<b>${__escapeHTMLSpecialChars(odds)}</b>" (the full text of the line with the error is "${__escapeHTMLSpecialChars(text)}"). The text after the "^" character defines the chance that that item will be selected (the higher the number the greater the chance). Odds declarations should only contain numbers, mathematical/logical operators, and are also allowed to have curly and square bracket blocks which will evaluate to number characters (these will be computed on-the-fly). If you'd like to use the "^" character literally (i.e. not to declare odds), you need to put a backslash character before it like so: "\\^"`, ctxInfo);
return false;
}
return {
odds,
textWithoutOdds: text.substr(0,i), // don't trim() - allow caller to decide, since different notations have difference whitespace preferences
};
}
}
return false;
}
function __oddsTextToNumber(rootNode, thisRef, text, ctxInfo={}) {
// optimisations:
if(text === "1") { // default value:
return 1;
} else if(!/[^0-9]/.test(text)) { // all digits:
return Number(text);
} else if(String(Number(text)) === text) {
return Number(text);
} else {
// complex expressions:
// NOTE: there's an important distinction between converting primitive declarations to numbers,
// and converting odds declarations to numbers. We only convert primitive declarations to
// numbers if Number.toString() would return the same string anyway. On the other hand, odds
// declarations can contain strings with math symbols
let evaluatedText = __evaluateText(rootNode, thisRef, text, ctxInfo);
if(evaluatedText === "false") evaluatedText = "0";
else if(evaluatedText === "true") evaluatedText = "1";
// TODO: check that this works
if(/[^0-9\/*&|%!.\(\)e\-]/.test(evaluatedText)) {
__perchanceError(`Your odds declaration: "<b>${__escapeHTMLSpecialChars(text)}</b>", evaluated to: "<b>${__escapeHTMLSpecialChars(evaluatedText)}</b>" which contains characters that aren't allowed in odds declarations. The text after the "^" character defines the chance that that item will be selected (the higher the number the greater the chance). Odds declarations should only contain numbers, mathematical/logical operators, and are also allowed to have curly and square bracket blocks which will evaluate to number characters (these will be computed on-the-fly). If you'd like to use the "^" character literally (i.e. not to declare odds), you need to put a backslash character before it like so: "\\^"`, ctxInfo);
return false;
}
let num = false;
try { num = eval(evaluatedText); } catch(e) {/* no need to catch */}
return num;
}
}
function __splitTextAtSquareBlocks(text, ctxInfo={}) {
if(!text.includes("[") && !text.includes("]")) return [text];
let escaped = false;
let unclosedBracketCount = 0;
let inJSExpr = false;
let inJSExprString1 = false, inJSExprString2 = false, inJSExprString3 = false;
let expressionArray = [];
for(let i = 0; i < text.length; i++) {
if(i !== 0) {
if(text[i-1] !== "\\") { escaped = false; }
}
if(text[i] === "\\") { escaped = !escaped; }
// skip strings in js expressions (square brackets in strings would mess
// with our unclosedBracketCount):
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "\"") { inJSExprString1 = true; continue; }
if(inJSExprString1 && !escaped && text[i] !== "\"") { continue; }
if(inJSExprString1 && !escaped && text[i] === "\"") { inJSExprString1 = false; continue; }
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "'") { inJSExprString2 = true; continue; }
if(inJSExprString2 && !escaped && text[i] !== "'") { continue; }
if(inJSExprString2 && !escaped && text[i] === "'") { inJSExprString2 = false; continue; }
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "`") { inJSExprString3 = true; continue; }
if(inJSExprString3 && !escaped && text[i] !== "`") { continue; }
if(inJSExprString3 && !escaped && text[i] === "`") { inJSExprString3 = false; continue; }
// skip escaped brackets within js expressions (i.e. within regex declarations):
if(inJSExpr && escaped && (text[i] === "[" || text[i] === "]")) { continue; }
if(!escaped && text[i] === "[") {
unclosedBracketCount++;
// if we weren't already inJSExpr, then this is the start of the a JSExpr:
if(!inJSExpr && i !== 0) {
expressionArray.push(text.substr(0, i));
text = text.substr(i);
i = 0; // will be incremented to 0 on next loop, as required (we've already processed the first char, "[")
}
inJSExpr = true;
continue;
}
if(!escaped && text[i] === "]") {
unclosedBracketCount--;
// we're only out of the js expression when we've closed all brackets
if(inJSExpr && unclosedBracketCount === 0) {
inJSExpr = false;
expressionArray.push(text.substr(0, i+1));
text = text.substr(i+1);
i = -1; // will be incremented to 0 on next loop as required (we haven't yet processed the first character of the new string (the character after "]"))
}
}
// last expression:
if(i === text.length-1 && text.length !== 0) {
expressionArray.push(text);
}
}
if(unclosedBracketCount !== 0) {
__perchanceError(`It appears that you've got a <b>mismatch in your opening and closing square brackets</b>${ctxInfo.declarationLineNumber ? ` on <b>line number ${ctxInfo.declarationLineNumber}</b>` : ""}. For each opening square bracket, there should be a closing one. If you'd like to use a <i>literal</i> square bracket (i.e. you want to actually display one, rather than using them to output a random list item, then you need to put a "backslash" before it like "\\[ ... \\]". Here's the text that seems to be causing the error: <blockquote>${__escapeHTMLSpecialChars(text)}</blockquote>`, ctxInfo);
return false;
}
return expressionArray;
}
function __getInlineFunctionDetails(lineText) {
let isAsync = false;
if(lineText.startsWith("async ")) {
isAsync = true;
lineText = lineText.substr(6);
}
let escaped = false;
let openIndex, opened = false;
let closeIndex, closed = false;
for(let i = 0; i < lineText.length; i++) {
if(i !== 0) {
if(lineText[i-1] !== "\\") { escaped = false; }
}
if(lineText[i] === "\\") { escaped = !escaped; }
if(lineText[i] === "(" && !escaped) { opened = true; openIndex = i; }
if(lineText[i] === ")" && !escaped && opened) { closed = true; closeIndex = i; }
if(lineText[i] === "=" && !escaped && opened && closed && lineText[i+1] === ">") {
let argString = lineText.substr(openIndex+1, closeIndex-openIndex-1);
let args = argString.split(",").map(a => a.trim());
let name = lineText.substr(0, openIndex).trim();
let body = lineText.substr(i+2).trim();
if(name.trim() === "") { return false; }
if(body.trim() === "") { return false; }
if(__areValidFunctionArguments(args) || argString === "") {
if(!__isValidJavaScriptIdentifier(name)) return false;
args = args.filter(a => a !== ""); // for zero arg case
return { args, name, body, isAsync };
}
}
}
return false;
}
function __getFunctionHeaderDetails(lineText) {
let isAsync = false;
if(lineText.startsWith("async ")) {
isAsync = true;
lineText = lineText.substr(6);
}
lineText = lineText.trim();
let l = lineText.length;
if(lineText[l-2] === "=" && lineText[l-1] === ">") {
lineText = lineText.substr(0, l-2).trim();
l = lineText.length;
} else {
return false;
}
let escaped = false;
let opened = false;
let openIndex;
for(let i = 0; i < lineText.length; i++) {
if(i !== 0) {
if(lineText[i-1] !== "\\") { escaped = false; }
}
if(lineText[i] === "\\") { escaped = !escaped; }
if(lineText[i] === "(" && !escaped) { opened = true; openIndex = i; }
if(lineText[i] === ")" && !escaped && opened) {
let argString = lineText.substr(openIndex+1, i-openIndex-1);
let args = argString.split(",").map(a => a.trim());
let name = lineText.substr(0, openIndex).trim();
if(name.trim() === "") { return false; }
if(__areValidFunctionArguments(args) || argString === "") {
if(!__isValidJavaScriptIdentifier(name)) return false;
args = args.filter(a => a !== ""); // for zero arg case
return { args, name, isAsync };
}
}
}
return false;
}
function __areValidFunctionArguments(args) {
for(let a of args) {
if(!__isValidJavaScriptIdentifier(a.replace("...","").replace("=",""))) {
return false;
}
}
return true;
}
function __isValidJavaScriptIdentifier(str) {
return /^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|null|this|true|void|with|await|break|catch|class|const|false|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\u
}
function __getPrimitiveNodeDetails(lineText) {
// NOTE: this function interprets some strings as numbers and casts them using Number()
// note that this is *just a shorthand*. The reason it's okay to do is because it
// only interprets strings as numbers if Number(that_number).toString() would return
// the same string anyway. If the user wants to do something like:
// prop = [this.age]/[this.height]
// then they should do this instead:
// prop = [this.age/this.height]
// because the prior will return something like "10/140" (i.e. a string, rather than a number)
// remember: the lineText could have square blocks which can of course have equals signs in them.
// But since primitive node keys can't be dynamic we can just return false if we see an unescaped square
// or curly bracket before we find an unescaped equals sign
let escaped = false;
for(let i = 0; i < lineText.length; i++) {
if(i !== 0) {
if(lineText[i-1] !== "\\") { escaped = false; }
}
if(lineText[i] === "\\") { escaped = !escaped; }
if((lineText[i] === "[" || lineText[i] === "{") && !escaped) {
// found square or curly block before unescaped equals sign, so it's not a primitive node
return false;
}
if(lineText[i] === "=" && !escaped) {
let key = lineText.substr(0, i).trim();
let valueText = lineText.substr(i+1).trim();
if(key === "" || valueText === "") {
return false;
}
let value;
if(valueText === String(Number(valueText))) {
value = Number(valueText);
} else if(valueText === "true") { // TODO: test that this works (with filter/where, etc.)
value = true;
} else if(valueText === "false") {
value = false;
} else {
value = valueText;
}
return {key, value};
}
}
return false;
}
// function isMostLikelyAnIntentionalNumericString(valueText) {
// if(isNaN(valueText)) { return false; }
// if(/[^0-9.\-]+/.test(valueText)) { return false; }
// if(valueText.split("-").length > 2) { return false; }
// if(valueText.split(".").length > 2) { return false; }
//
// if(valueText.split("-").length === 2) {
// if(valueText[0] !== "-") { return false; }
// if(valueText[0] === "-" && valueText[1] === ".") { return false; }
// }
//
// if(valueText.split(".").length === 2) {
// if(valueText[0] === ".") { return false; }
// if(valueText[valueText.length-1] === ".") { return false; }
//
// let beforeDecimal = valueText.split(".")[0];
// let afterDecimal = valueText.split(".")[1];
// if(afterDecimal[afterDecimal.length-1] === "0") { return false; } // e.g. 20.0 shouldn't be treated as a number (they'd have just put in "20")
// if(beforeDecimal[0] === "0" && beforeDecimal.length > 1) { return false; } // e.g. 020 shouldn't be treated as a number
// }
//
// return true;
//
// }
function __splitTextAtCurlyBlocks(text, ctxInfo={}) {
// remember: we must ignore curly brackets if we're inside a js expression
if(!text.includes("{") && !text.includes("}")) return [text];
let escaped = false;
let lastCurlyOpenIndex = null;
let inJSExpr = false;
let unclosedSquareBracketCount = 0;
let inJSExprString1 = false;
let inJSExprString2 = false;
let inJSExprString3 = false;
let inCurlyExpr = false;
let unclosedCurlyBracketCount = 0;
let blocks = [];
for(let i = 0; i < text.length; i++) {
if(i !== 0) {
if(text[i-1] !== "\\") { escaped = false; }
}
if(text[i] === "\\") { escaped = !escaped; }
////////////////////////////////////
// skip strings in js expressions //
////////////////////////////////////
//(square brackets in strings would mess with our unclosedSquareBracketCount which we use to skip js expressions):
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "\"") { inJSExprString1 = true; continue; }
if(inJSExprString1 && !escaped && text[i] !== "\"") { continue; }
if(inJSExprString1 && !escaped && text[i] === "\"") { inJSExprString1 = false; continue; }
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "'") { inJSExprString2 = true; continue; }
if(inJSExprString2 && !escaped && text[i] !== "'") { continue; }
if(inJSExprString2 && !escaped && text[i] === "'") { inJSExprString2 = false; continue; }
if(inJSExpr && !inJSExprString1 && !inJSExprString2 && !inJSExprString3 && !escaped && text[i] === "`") { inJSExprString3 = true; continue; }
if(inJSExprString3 && !escaped && text[i] !== "`") { continue; }
if(inJSExprString3 && !escaped && text[i] === "`") { inJSExprString3 = false; continue; }
// skip escaped brackets within js expressions (i.e. escaped brackets within regex declarations):
if(inJSExpr && /*--->*/escaped/*<---*/ && (text[i] === "[" || text[i] === "]")) { continue; }
/////////////////////////////////////
// skip JS expressions //
/////////////////////////////////////
// note that we're guarunteed to NOT be in a string for this block (since the
// above block skips strings while we're in js expressions)
if(!inJSExpr && text[i] === "[" && !escaped) {
inJSExpr = true;
unclosedSquareBracketCount++;
continue;
}
if(inJSExpr && text[i] === "[") { // `escaped` is irrelevant, we're inJSExpr and not in string
unclosedSquareBracketCount++;
continue;
}
if(inJSExpr && text[i] === "]") { // `escaped` is irrelevant, we're inJSExpr and not in string
unclosedSquareBracketCount--;
if(unclosedSquareBracketCount === 0) { inJSExpr = false; }
}
//////////////////////////////////////
// extract curly blocks //
//////////////////////////////////////
if(text[i] === "{" && !escaped && !inJSExpr) {
unclosedCurlyBracketCount++;
// if we're not yet in a curly expression, then this is the start of one:
if(!inCurlyExpr && i !== 0) {
blocks.push(text.substr(0, i));
text = text.substr(i);
i = 0;
}
inCurlyExpr = true;
continue;
}
if(text[i] === "}" && !escaped && !inJSExpr) {
unclosedCurlyBracketCount--;
// we're only out of the curly expression when we've closed all curly brackets that were opened
if(inCurlyExpr && unclosedCurlyBracketCount === 0) {
blocks.push(text.substr(0, i+1));
text = text.substr(i+1);
i = -1;
inCurlyExpr = false;
}
}
// add last expression (if it didn't end in a curly block - in that case text.length === 0)
if(i === text.length-1 && text.length !== 0) {
blocks.push(text);
}
}
if(unclosedSquareBracketCount !== 0) {
__perchanceError(`It appears that you've got <b>a mismatch in your opening and closing square brackets</b>${ctxInfo.declarationLineNumber ? ` on <b>line number ${ctxInfo.declarationLineNumber}</b>` : ""}. For each opening square bracket, there should be a closing one. If you'd like to use a <i>literal</i> square bracket (i.e. you want to actually display one, rather than using them to output a random list item, then you need to put a "backslash" before it like "\\[ ... \\]". Here's the text that seems to be causing the error: <blockquote>${__escapeHTMLSpecialChars(text)}</blockquote>`, ctxInfo);
return ["(syntax error)"];
}
if(unclosedCurlyBracketCount !== 0) {
__perchanceError(`It appears that you've got <b>a mismatch in your opening and closing curly brackets</b>${ctxInfo.declarationLineNumber ? ` on <b>line number ${ctxInfo.declarationLineNumber}</b>` : ""}. For each opening curly bracket, there should be a closing one. If you'd like to use a <i>literal</i> curly bracket (i.e. you want to actually display one, rather than using them to do {import:noun} and stuff like that, then you need to put a "backslash" before it like "\\{ ... \\}". Here's the text that seems to be causing the error: <blockquote>${__escapeHTMLSpecialChars(text)}</blockquote>`, ctxInfo);
return ["(syntax error)"];
}
return blocks;
}
function __splitTextAtAllBlocks(text, ctxInfo={}) {
let curlySplitParts = __splitTextAtCurlyBlocks(text, ctxInfo);
let blocks = []; // this array will contain curly blocks, text blocks, and square blocks
for(let part of curlySplitParts) {
if(part[0] !== "{") {
let squareSplitParts = __splitTextAtSquareBlocks(part, ctxInfo);
if(squareSplitParts) {
blocks.push(...squareSplitParts);
} else {
console.error("Something is wrong?");
}
} else {
blocks.push(part);
}
}
return blocks;
}
function __stripCommentFromLine(line) {
// NOTE: comment must either start at start of line, or have a space before it.
// e.g. in http://example.com , "//example.com" isn't considered a comment because
// it doesn't have a preceding space
let i, escaped = false;
for(i = 0; i < line.length; i++) {
if(i !== 0) {
if(line[i-1] !== "\\") { escaped = false; }
}
if(line[i] === "\\") { escaped = !escaped; }
if(line[i] === "/" && !escaped && line[i+1] === "/") {
if(i === 0) {
break;
} else {
if(line[i-1] === " " || line[i-1] === "\t") {
break;
}
}
}
}
return line.substr(0,i);
}
function __normaliseLineIndentsToTabs(line) {
let i, eqSpaceCount = 0; //equivalent spaces (1 tab = 2 spaces)
for(i = 0; i < line.length; i++) {
if(line[i] === " ") { eqSpaceCount++; }
else if(line[i] === "\t") { eqSpaceCount += 2; }
else { break; }
}
if(eqSpaceCount%2 !== 0) {
return false; //odd number of spaces
} else {
return "\t".repeat(parseInt(eqSpaceCount/2)) + line.substr(i);
}
}</script> <script>Object.defineProperty(String.prototype, "titleCase", {get:__titleCaseMethod});
Object.defineProperty(Number.prototype, "titleCase", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "titleCase", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "sentenceCase", {get:__sentenceCaseMethod});
Object.defineProperty(Number.prototype, "sentenceCase", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "sentenceCase", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "upperCase", {get:__upperCaseMethod});
Object.defineProperty(Number.prototype, "upperCase", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "upperCase", {get:function(){return this.valueOf();}})
Object.defineProperty(String.prototype, "lowerCase", {get:__lowerCaseMethod});
Object.defineProperty(Number.prototype, "lowerCase", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "lowerCase", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "replaceText", {value:__replaceTextMethod});
Object.defineProperty(Number.prototype, "replaceText", {value:__replaceTextMethod});
Object.defineProperty(Boolean.prototype, "replaceText", {value:__replaceTextMethod});
Object.defineProperty(String.prototype, "pastTense", {get:__pastTenseMethod});
Object.defineProperty(Number.prototype, "pastTense", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "pastTense", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "presentTense", {get:__presentTenseMethod});
Object.defineProperty(Number.prototype, "presentTense", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "presentTense", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "futureTense", {get:__futureTenseMethod});
Object.defineProperty(Number.prototype, "futureTense", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "futureTense", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "getSelf", {get:function() { return this+""; }});
Object.defineProperty(Number.prototype, "getSelf", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "getSelf", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "getOdds", {get:function() { return 1; }});
Object.defineProperty(Number.prototype, "getOdds", {get:function(){ return 1; }});
Object.defineProperty(Boolean.prototype, "getOdds", {get:function(){ return 1; }});
// WHAT IS THIS?
Object.defineProperty(String.prototype, "getName", {get:function() {
return this+"";
}});
Object.defineProperty(Number.prototype, "getName", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "getName", {get:function(){return this.valueOf();}})
Object.defineProperty(String.prototype, "evaluateItem", {get:function() {
let r = window.__currentEvaluateSquareBlockRoot ? window.__currentEvaluateSquareBlockRoot : root; // so imported generators use their own root
let out = __evaluateText(r, r, this.valueOf(), {declarationLineNumber:null});
if(window.generatorLastEditTime > 1716337636385) { // May 22nd 2024 bugfix, ensures only generators edited after this time get it, to prevent breaking old generators that rely on the bug
out = __processEscapedCharacters(out);
}
if(String(Number(out)) === out) {
out = Number(out);
}
return out;
}});
Object.defineProperty(Number.prototype, "evaluateItem", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "evaluateItem", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, Symbol.toPrimitive, {value:function(hint) {
return this.valueOf();
}});
// i don't think these two are necessary?
Object.defineProperty(Number.prototype, Symbol.toPrimitive, {value:function(hint) {
return this.valueOf();
}});
Object.defineProperty(Boolean.prototype, Symbol.toPrimitive, {value:function(hint) {
return this.valueOf();
}});
Object.defineProperty(String.prototype, "selectMany", {value:__selectManyMethodStringNum});
Object.defineProperty(Number.prototype, "selectMany", {value:__selectManyMethodStringNum});
Object.defineProperty(Boolean.prototype, "selectMany", {value:__selectManyMethodStringNum});
function __selectManyMethodStringNum(num) {
let arr = [];
for(let i = 0; i < num; i++) {
arr.push(this.selectOne);
}
// overwrite default array behaviour (selects a random one)
// in the case where they don't specify a join() after the repeat:
arr.toString = function() { return this.join(""); };
return arr;
}
Object.defineProperty(String.prototype, "selectOne", {get:function() {
let r = window.__currentEvaluateSquareBlockRoot ? window.__currentEvaluateSquareBlockRoot : root; // so imported generators use their own root
let out = __evaluateText(r, r, this.valueOf(), {declarationLineNumber:null}); // so you can write e.g. [n = "{1-20}".selectOne]
if(window.generatorLastEditTime > 1716337636385) { // May 22nd 2024 bugfix, ensures only generators edited after this time get it, to prevent breaking old generators that rely on the bug
out = __processEscapedCharacters(out);
}
if(String(Number(out)) === out) {
out = Number(out);
}
return out;
}});
Object.defineProperty(Number.prototype, "selectOne", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "selectOne", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "singularForm", {get:function(n) {
let input = this;
let output = __nlpCompromise(input).nouns().toSingular().out("text");
return output === "" ? input : output;
}});
Object.defineProperty(Number.prototype, "singularForm", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "singularForm", {get:function(){return this.valueOf();}});
Object.defineProperty(String.prototype, "pluralForm", {get:function(n) {
let input = this;
let output = __nlpCompromise(input).nouns().toPlural().out("text");
return output === "" ? input : output;
}});
Object.defineProperty(Number.prototype, "pluralForm", {get:function(){return this.valueOf();}});
Object.defineProperty(Boolean.prototype, "pluralForm", {get:function(){return this.valueOf();}});
// TODO***: add the rest of the node methods here and to Array (e.g. pastTense, etc.)
// Object.defineProperty(String.prototype, "shuffleCharacters", {value:function(numShuffles) {
//
// if(numShuffles === undefined) {
//
// var array = this.split("");
// var currentIndex = array.length, temporaryValue, randomIndex;
// // While there remain elements to shuffle...
// while (0 !== currentIndex) {
// // Pick a remaining element...
// randomIndex = Math.floor(Math.random() * currentIndex);
// currentIndex -= 1;
// // And swap it with the current element.
// temporaryValue = array[currentIndex];
// array[currentIndex] = array[randomIndex];
// array[randomIndex] = temporaryValue;
// }
// return array.join("")
//
// } else {
//
// let array = this.split("");
// for(let i = 0; i < numShuffles; i++) {
// let i1 = Math.floor(array.length*Math.random());
// let i2 = Math.floor(array.length*Math.random());
// let c1 = array[i1];
// let c2 = array[i2];
// array[i1] = c2;
// array[i2] = c1;
// }
// return array.join("");
// // TODO: why not join back into a string?
//
// }
//
// }});</script> <script>Array.prototype.toString = function () {
// default behaviour of arrays when converting to string is to choose a random element.
// altering the toString method is how you make it not do this by default (this is how selectMany works)
// let items = [];
// for(let item of this) {
// items.push(item+"");
// }
// return chooseRandomTextByOdds(window.root, window.root, items);
if(this.length === 0) return "";
return this.selectOne+""; // should this be `this.selectOne.toString({keepEscapes:true})` ?
};
Object.defineProperty(Array.prototype, "getOdds", {get:function() { return 1; }});
Object.defineProperty(Array.prototype, "getSelf", {get:function() { return this; }});
Object.defineProperty(Array.prototype, "getLength", {get:function() { return this.length; }});
Object.defineProperty(Array.prototype, "consumableList", {get:function() {
let proxy = new Proxy(this, {
// alreadyConsumedItems: new Set(),
alreadyConsumedIndices: new Set(), // NOTE: we need this (unlike in the addNodeMethods.js case where we use alreadyConsumedItems) because an array can contain duplicates of the same item: lemmy.world/post/6207000 but we should treat them as separate items.
get: function(target, property) {
if(property === "selectOne") {
let selectedNode = __arraySelectOneMethod.bind(proxy)();
// if(selectedNode) { // consumption is done in __arraySelectOneMethod now
// this.alreadyConsumedItems.add(selectedNode);
// }
return selectedNode;
}/* else if(property === "$alreadyConsumedItems") {
return this.alreadyConsumedItems;
}*/ else if(property === "$alreadyConsumedIndices") {
return this.alreadyConsumedIndices;
} else if(property === "getLength") {
// return target.length - this.alreadyConsumedItems.size;
return target.length - this.alreadyConsumedIndices.size;
} else {
// TODO: strangely, the return value here seems to be automatically bound to this proxy.
// so it also handles toString and valueOf methods like we want it to
return target[property];
}
},
});
return proxy;
}});
Object.defineProperty(Array.prototype, "selectAll", {get: function() {
return this;
}});
Object.defineProperty(Array.prototype, "selectMany", {value:__selectManyMethod});
Object.defineProperty(Array.prototype, "selectUnique", {value: function(...args) {
return this.consumableList.selectMany(...args);
}});
let __arraySelectOneMethod = function() {
//return this[Math.floor(Math.random()*this.length)];
let candidates = []; /*{item, odds}*/
for(let i = 0; i < this.length; i++) {
if(this.$alreadyConsumedIndices && this.$alreadyConsumedIndices.has(i)) continue;
let a = this[i];
if(a.$nodeType) { // if it's a node
candidates.push({item:a, odds:a.$odds, index:i});
} else if(typeof a === 'string') {
let details = __getTextOddsDetails(a);
if(details && (details.odds.includes("[") || details.odds.includes("{"))) details.odds = details.odds.evaluateItem; // don't need the if statement, but it's better for performance
let odds = details ? Number(details.odds) : 1; // <-- this doesn't allow dynamic odds notation in arrays. Can always add it later if it turns out that it would be useful.
if(odds < 0 || isNaN(odds)) {
__perchanceError(`The odds of ${a} has evaluated to ${odds < 0 ? "a negative number" : "a non-number"}. It's item number ${i} in the list (the list that starts with ${this[0]}). You may have created this list with selectAll or selectUnique.`);
}
let item = details ? details.textWithoutOdds : a;
candidates.push({item, odds, index:i});
} else {
candidates.push({item:a, odds:1, index:i});
}
}
// if(this.$alreadyConsumedItems) { // this doesn't make sense because an array can contain duplicates of the same node: lemmy.world/post/6207000
// candidates = candidates.filter(c => !this.$alreadyConsumedItems.has(c.item));
// }
let oddsTotal = candidates.reduce((a,v) => { return a+v.odds; }, 0);
let stopOddsSum = oddsTotal*Math.random();
let oddsRunningSum = 0;
for(let c of candidates) {
oddsRunningSum += c.odds;
if(oddsRunningSum >= stopOddsSum) {
if(this.$alreadyConsumedIndices) this.$alreadyConsumedIndices.add(c.index);
return c.item;
}
}
if(this.$alreadyConsumedIndices) {
__perchanceError("It looks like your <code>consumableList</code> ran out of items. This is the list in question: "+this.slice(0, 100).join(", "));
} else {
__perchanceError("Something went wrong! <code>Error: selectOne from array returned nothing.</code> Please report this on <a href='https://lemmy.world/c/perchance'>lemmy.world/c/perchance</a> since it's likely a bug with the engine.");
}
};
Object.defineProperty(Array.prototype, "selectOne", {get:__arraySelectOneMethod});
Object.defineProperty(Array.prototype, Symbol.toPrimitive, {value:function(hint) {
return this.toString();
}});
// don't need this because we've got joinItems, right?
// Object.defineProperty(Array.prototype, "evaluateItem", {get:function() {
// return this.toString();
// }});
Object.defineProperty(Array.prototype, "joinItems", {value:function(str="") {
return this.join(str);
}});
Object.defineProperty(Array.prototype, "sumItems", {get:function() {
return this.reduce((a, v) => a+v.evaluateItem, 0);
}});
// TODO***: make all methods configurable like in nodeMethods?
Object.defineProperty(Array.prototype, "pluralForm", {get:function() {
return this.map(a => a.pluralForm);
}});
Object.defineProperty(Array.prototype, "singularForm", {get:function() {
return this.map(a => a.singularForm);
}});
Object.defineProperty(Array.prototype, "pastTense", {get:function() {
return this.map(a => a.pastTense);
}});
Object.defineProperty(Array.prototype, "presentTense", {get:function() {
return this.map(a => a.presentTense);
}});
Object.defineProperty(Array.prototype, "futureTense", {get:function() {
return this.map(a => a.futureTense);
}});
Object.defineProperty(Array.prototype, "titleCase", {get:function() {
return this.map(a => a.titleCase);
}});
Object.defineProperty(Array.prototype, "upperCase", {get:function() {
return this.map(a => a.upperCase);
}});
Object.defineProperty(Array.prototype, "lowerCase", {get:function() {
return this.map(a => a.lowerCase);
}});
Object.defineProperty(Array.prototype, "sentenceCase", {get:function() {
return this.map(a => a.sentenceCase);
}});
Object.defineProperty(Array.prototype, "replaceText", {value:function(p1, p2, p3) {
return this.map(a => a.replaceText(p1, p2, p3));
}});</script> <script>let __curlyFunctions = [__curlyFunction_Or, __curlyFunction_Import, __curlyFunction_Range, __curlyFunction_S, __curlyFunction_A];
function __curlyFunction_Import(rootNode, thisRef, text, leftBlocks, rightBlocks, ctxInfo) {
if(!text.includes(":")) return false;
let match = /^import:([a-z0-9\-]+)$/.exec(text);
if(!match) {
return false;
} else {
let moduleName = match[1];
let moduleRef = rootNode.$moduleSpace[moduleName];
if( moduleRef ) {
return moduleRef;
} else {
__perchanceError(`You've tried to import a generator that doesn't exist. The generator named '${__escapeHTMLSpecialChars(moduleName)}' was not found. You can see for yourself by visiting that generator: <a href="/${moduleName}">${moduleName}</a>. There's a <a href="/useful-generators">list of useful generators</a> that may help you find what you're looking for.`, ctxInfo);
return `(cannot find generator '${moduleName}')`;
}
}
}
function __curlyFunction_A(rootNode, thisRef, text, leftBlocks, rightBlocks) {
if(text === "a" || text === "A") {
let right = __removeHtmlTagsFromBlocksArray(rightBlocks); //rightBlocks.filter(b => b.replace(/<[^>]+?>/g, '').trim() !== "");
if(right[0] === undefined || (right[0] && (right[0].trim()[0] === "{"|| right[0].trim()[0] === "["))) {
return text === "a" ? "{a}" : "{A}";
} else {
let aOrAn = __AvsAnSimple.query(right[0].trim().replace(/<[^>]+?>/g, '').split(/[\s$%&*@#,!=+\-.?:;'"(){}\[\]\/<>`~_|]/g)[0].trim());
if(text === "A") {
if(aOrAn === "a") aOrAn = "A";
else aOrAn = "An";
}
return aOrAn;
}
} else {
return false;
}
}
function __curlyFunction_Range(rootNode, thisRef, text, leftBlocks, rightBlocks) {
if(!text.includes("-")) return false;
let match;
// TODO: written numbers: {twenty-fifty five} ?
// TODO: custom increments: {0-100:0.5} ? not sure about notation yet
// {1-10}
match = /^([0-9]+)-([0-9]+)$/.exec(text);
if(match) {
let min = Number(match[1]);
let max = Number(match[2]);
if(min > max) { [min, max] = [max, min]; }
return Math.floor(Math.random() * (max - min + 1) + min); // we do not talk about what was here before
}
// {a-z}
match = /^([a-z])-([a-z])$/.exec(text);
if(match) {
let min = Number(match[1].charCodeAt(0));
let max = Number(match[2].charCodeAt(0));
if(min > max) { [min, max] = [max, min]; }
return String.fromCharCode( min+Math.round(Math.random()*(max-min)) );
}
// {A-Z}
match = /^([A-Z])-([A-Z])$/.exec(text);
if(match) {
let min = Number(match[1].charCodeAt(0));
let max = Number(match[2].charCodeAt(0));
if(min > max) { [min, max] = [max, min]; }
return String.fromCharCode( min+Math.round(Math.random()*(max-min)) );
}
return false;
}
function __curlyFunction_S(rootNode, thisRef, text, leftBlocks, rightBlocks) {
// there are {1..10} person{s} <-- can't do this, must manually handle the 2 cases
// I have {1..10} coin{s} <-- can do this :)
if(text === "s") {
// TODO: fix this with some proper NLP to handle written numbers: "twenty five"
// search through leftBlocks backwards for the first number.
// if a non-text block is found before a number is, return "{s}"
for(let i = leftBlocks.length-1; i >= 0; i--) {
let block = leftBlocks[i];
if(block[0] === "{" || block[0] === "[") {
return "{s}";
}
let numbers = block.split(/[\s,!?\-()/.]+/).filter(a => /[0-9]+/.test(a));
if(numbers.length > 0) {
let num = Number(numbers[numbers.length-1]);
return num === 1 ? "" : "s";
}
}
}
return false;
}
function __curlyFunction_Or(rootNode, thisRef, text, leftBlocks, rightBlocks, ctxInfo={}) {
if(!text.includes("|")) return false;
// no need to evaluate the arguments. we can just extract them (with their odds)
// and select a random one. We could evaluate the args but in that case it would be
// important that we extract all the *arguments* first (and their odds), then
// choose a random one (otherwise NEW "|" and "^" characters could end up in the evaluated text)
// and mess up our interpretation of the arguments.
// pulled the bulk of this curlyFunction_Or function out into its own function for use elsewhere:
let args = __splitUpCurlyOrBlock(text, ctxInfo);
if(args === false) return false;
// now we need to extract odds and choose a random one
// note that in curly "or" notation, *spaces matter* before and after the text.
// thus we don't trim the string after extracting the odds.
return __chooseRandomTextByOdds(rootNode, thisRef, args);
// TODO: sort out Array.toString
// LOOK AT Array.toString definition - try to extract out a common function
// (or you could just call) .toString on `parts` lol
// hmm. Array.toString relies on window.root being available which isn't great.
// I think first step is to test whether it works, then extract out as much as I
// can into functions. Array.toString is more of a handy hack - I don't want to rely
// on it if I can help it.
}
function __splitUpCurlyOrBlock(text, ctxInfo={}) {
// note this returns false if it's not a valid curly OR block.
// also, it expects that the outer curly brackets have already been removed
// the easiest way to be find all the unescaped "|" characters is to split it
// up and then search through *only text blocks*:
let blocks = __splitTextAtAllBlocks(text, ctxInfo);
let args = [];
let currentArg = "";
for(let j = 0; j < blocks.length; j++) {
let block = blocks[j];
if(block[0] !== "{" && block[0] !== "[") {
let escaped = false;
for(let i = 0; i < block.length; i++) {
if(i === 0) {
// if we're at the start of the block, it's not escaped:
escaped = false;
} else {
// if we're not at start, it's not escaped if the last char wasn't a backslash:
if(block[i-1] !== "\\") { escaped = false; }
}
// toggle escapedness at each consecutive backslash:
if(block[i] === "\\") { escaped = !escaped; }
if(block[i] === "|" && !escaped) {
currentArg += block.substr(0,i);
args.push(currentArg);
currentArg = "";
block = block.substr(i+1);
i = -1; // will get incremented to zero on loop
} else if(i === block.length-1) {
currentArg += block;
}
}
if(j === blocks.length-1) {
args.push(currentArg);
}
} else {
currentArg += block;
if(j === blocks.length-1) {
args.push(currentArg);
}
}
}
if(args.length === 0 || args.length === 1) {
// this is not the correct curly function to interpret this curly block
return false;
}
return args;
}
function __chooseRandomTextByOdds(rootNode, thisRef, textArray, ctxInfo={}) {
// extract odds
let items = [];
for(let text of textArray) {
text = text+""; // cast to string (needed for Array.toString);
let odds, textWithoutOdds;
let details = __getTextOddsDetails(text);
// TODO: fix this up to suit the new way of doign things
if(!details) {
textWithoutOdds = text;
odds = 1;
items.push({text:textWithoutOdds, odds});
continue;
}
textWithoutOdds = details.textWithoutOdds;
odds = details.odds;
if(typeof odds === 'string' && (odds[0] === "[" || /[0-9]/.test(odds[0]) )) { // if it's a dynamic odds expression:
let evaluatedOdds = __oddsTextToNumber(rootNode, thisRef, odds);
if(evaluatedOdds < 0) {
__perchanceError(`The dynamic odds expression "${__escapeHTMLSpecialChars(odds)}" in ${__escapeHTMLSpecialChars("{"+textArray.join("|")+"}")} resolved to a *negative* number. Odds must be positive numbers, since it doesn't make sense for something to have a negative chance of being selected. Remember that you can try executing this odds expression in the console (bottom-left panel in the editor interface) and see what it yeilds. Also remember that "^" is a special character that is used to declare the probability that the item will be selected, and if you want to use it as a literal character (i.e. not to declare odds), then you need to put a "backslash" before it like so: "\\^"`, ctxInfo);
return false;
}
if(evaluatedOdds === false) {
__perchanceError(`The dynamic odds expression "${__escapeHTMLSpecialChars(odds)}" in ${__escapeHTMLSpecialChars("{"+textArray.join("|")+"}")} didn't resolve to a number. Remember that you can try executing this odds expression in the console (bottom-left panel in the editor interface) and see what it yeilds. Also remember that "^" is a special character that is used to declare the probability that the item will be selected, and if you want to use it as a literal character (i.e. not to declare odds), then you need to put a "backslash" before it like so: "\\^"`, ctxInfo);
return false;
}
odds = evaluatedOdds;
}
items.push({text:textWithoutOdds, odds});
}
// select one based on odds
let oddsSum = 0;
let oddsArray = [];
for(let item of items) {
oddsSum += item.odds;
oddsArray.push(item.odds);
}
if(items.length === 0) {
console.warn(`Warning: Tried to get random selection from an empty array?`);
return "";
}
// choose random position in oddsSum range
let oddsSumStopPoint = oddsSum*Math.random();
let selectedItem;
let oddsSumForChoice = 0;
for(let i = 0; i < items.length; i++) {
oddsSumForChoice += oddsArray[i];
if(oddsSumForChoice >= oddsSumStopPoint) {
selectedItem = items[i];
break;
}
}
return selectedItem.text;
}
// doesn't need to be perfect! just a rough clean to help the {a/A} blocks
function __removeHtmlTagsFromBlocksArray(blocks) {
const joinTag = "sdoxci892387uoooJOINoooTAGooohkdsiyygjd970sjgs";
let str = blocks.join(joinTag);
let charArr = str.split("");
let inTag = false;
for(let i = 0; i < charArr.length; i++) {
if(charArr[i] === "<" && !inTag) {
inTag = true;
charArr[i] = "";
} else if(charArr[i] === ">" && inTag) {
inTag = false;
charArr[i] = "";
} else if(inTag) {
charArr[i] = "";
}
}
return charArr.join("").split(joinTag).filter(c => c.trim() !== "");
}</script> <script>let ___alreadyAttachedSaveHandler = false;
let ___postMessagesRecieved = 0;
let isFirstUpdate = true
window.moduleSpace = {};
async function ___updateOutput(outputTemplate) {
document.querySelector("#output-container").innerHTML = outputTemplate;
// We need to get the MutationObserver to ignore all of the nodes we just added because
// the MutationObserver is just for "dynamically added" nodes (i.e. added with custom javascript),
// and the nodes that we just added will be rendered by the `update()` function, of course.
// (we wouldn't actually need to do this if this function weren't async)
let allNewTextNodes = ___getAllTextNodeDescendents(document.querySelector("#output-container")); // this function adds the text nodes to the `allTextNodes` array
for(let textNode of allNewTextNodes) {
textNode.___alreadyRenderedPerchanceCode = true; // note that we haven't actually rendered it yet, but we're just about to (need to get in before MutationObserver, since this function is async)
}
// Need to do the same for attributes:
for(let el of Array.from(document.querySelectorAll("#output-container *"))) {
for(let attr of Array.from(el.attributes)) {
attr.___alreadyRenderedPerchanceCode = true;
}
}
___templatedNodes = [];
___addNodeTemplates(document.querySelector("#output-container"));
for(let element of Array.from(document.querySelectorAll("#output-container *"))) {
___reAttachDomElementEventsWithRoot(element);
}
await ___executeScriptTags();
update();
}
async function ___executeScriptTags(containerSelectorOrEl = "#output-container") {
let container;
if(typeof containerSelectorOrEl === 'string') container = document.querySelector(containerSelectorOrEl);
else container = containerSelectorOrEl;
let scriptTags = [];
if(container.tagName.toLowerCase() === 'script') {
scriptTags = [container];
} else {
scriptTags = container.getElementsByTagName('script');
}
for (var i = 0; i < scriptTags.length ; i++) {
await ___executeScriptTag(scriptTags[i]);
}
}
function ___getAllMatches(str, regex) {
const matches = [];
let m;
while(1) {
m = regex.exec(str);
if(m) matches.push(m);
else break;
}
return matches;
}
// For all Safari versions before 17.4:
let ___withFunctionScopeFixRequired = false;
try {
let obj = {a7d638p364:0};
with(obj) {
function fn() {
return a7d638p364+1;
}
fn();
}
} catch(e) {
___withFunctionScopeFixRequired = true;
}
async function ___executeScriptTag(oldScript) {
let parent = oldScript.parentElement;
let newScript = document.createElement('script');
// had to use hasAttribute instead of simple property access because firefox defaults to script.async=true for dynamically-created script tags
newScript.async = oldScript.hasAttribute("async"); //oldScript.async;
newScript.type = oldScript.type;
let loadPromise;
if(oldScript.src && !newScript.async && !newScript.defer) {
loadPromise = new Promise(resolve => {newScript.onload = resolve; newScript.onerror = resolve; });
}
if(oldScript.src) {
newScript.src = oldScript.src;
} else {
if(newScript.type === "module") {
newScript.textContent = oldScript.textContent;
} else {
let scriptText = oldScript.textContent;
// Apply Safari `with` statement fix if needed:
if(___withFunctionScopeFixRequired) {
try {
let topLevelListNamesRegex = new RegExp(window.root.getSelf.getAllKeys.join("|"));
if(topLevelListNamesRegex.test(scriptText)) { // only if it contains a list name - to reduce chance of introducing bugs, since regex below is not full-proof.
scriptText = scriptText.replace(/(\n\s*|^\s*)(async |)function\s+([a-zA-Z0-9_$]+?)(\s*\(.*\)\s*\{)/g, "$1var $3 = $2function$4");
}
} catch(e) {
console.error("Error while applying Safari with statement fix:", r);
}
}
//newScript.textContent = "with(window.root.___proxyTarget.obj){"+scriptText+"}";
// The following monstrosity replaces the above commented out line due to the fact that block-scoped declarations (let, const, async function) get trapped by the `with` statement's block. False positives for the regex are fine (e.g. if it grabs non-variables from strings that look like declarations). I'm also grabbing `function` in case I ever allow people to use strict mode (the current `with` precludes it) since in strict mode `function`s are block scoped.
let potentialNewGlobalVariableNames = ___getAllMatches(scriptText, /[\s;](?:var|let|const)\s+([^\s]+?)\s*=/g).map(r => r[1]);
potentialNewGlobalVariableNames.push(...___getAllMatches(scriptText, /[\s;](?:function|async function)\*?\s+([^\s]+?)\s*\(/g).map(r => r[1]));
potentialNewGlobalVariableNames.push(...___getAllMatches(scriptText, /[\s;]class\s+([^\s]+?)\s*\{/g).map(r => r[1]));
newScript.textContent = `
with(window.root.___proxyTarget.obj){
${scriptText};
for(let name of ${JSON.stringify(potentialNewGlobalVariableNames)}) {
try { let val = eval(name); window[name] = val; } catch(e) {}
}
}
`;
}
}
if(!newScript.type) {
newScript.setAttribute('type','text/javascript');
}
parent.insertBefore(newScript, oldScript);
parent.removeChild(oldScript);
if(loadPromise) await loadPromise;
return newScript;
}
let ___templatedNodes; // REMEMBER: order is important for functions like .id()
function ___addNodeTemplates(domNode) {
var nodes = domNode.childNodes;
for (var i = 0, m = nodes.length; i < m; i++) {
var n = nodes[i];
if(n.nodeType !== n.TEXT_NODE && n.nodeName !== "#comment") {
___templatedNodes.push( ...___addAttributeTemplateToEl(n) );
}
if (n.nodeType == n.TEXT_NODE && n.parentNode.tagName !== "SCRIPT" && n.parentNode.nodeName !== "STYLE") {
// we found a text node, so lets break it into an expression array
// and wrap expressions in spans (the "template span")
let arr = __splitTextAtAllBlocks(n.textContent);
if(!arr) {
__perchanceError(`Some text has caused an error with the compiler. If you can, please post a bug report with a link to your generator on <a href="https://lemmy.world/c/perchance">lemmy.world/c/perchance</a> to help me fix this bug. Thanks! Here's the text that caused this issue: ${__escapeHTMLSpecialChars(n.textContent)}`);
return;
}
if(arr.length === 0) {
if(n.textContent.trim() !== "") {
__perchanceError(`Something's not right. Some text has caused an error with the compiler. If you can, please post a bug report with a link to your generator on <a href="https://lemmy.world/c/perchance">lemmy.world/c/perchance</a> to help me fix this bug. A non-empty text node resulted in an empty block array. This is probably a problem with 'splitTextAtAllBlocks'. Maybe some 'continue' statements skipping over things that they shouldn't?`);
}
continue;
}
if(arr.length > 1 || arr[0][0] === "[" || arr[0][0] === "{") {
//let exprSpan = document.createElement('span');
// exprSpan.className = "__perchance_textnode_expression";
// exprSpan.dataset.perchanceExpression = arr.join("");
//exprSpan.innerText = arr.join("");
//n.parentNode.insertBefore(exprSpan, n);
//n.parentNode.removeChild(n);
___templatedNodes.push({oldNodes:[n], parentNode:n.parentNode, previousSibling:n.previousSibling, nextSibling:n.nextSibling, type:"textNode", text:arr.join("")});
}
// just regular text:
if(arr.length === 1 && arr[0][0] !== "[" && arr[0][0] !== "{") {
n.nodeValue = __processEscapedCharacters(n.nodeValue);
}
} else {
___addNodeTemplates(n);
}
}
}
function ___collectTemplatableTextChunks(domNode, foundChunks) {
if(!foundChunks) foundChunks = [];
var nodes = domNode.childNodes;
for (var i = 0, m = nodes.length; i < m; i++) {
var n = nodes[i];
if(n.nodeType !== n.TEXT_NODE && n.nodeName !== "#comment") {
for(let attribute of Array.from(n.attributes)) {
if(___isTemplatableAttributeName(attribute.name)) {
// we found a templatable attribute
foundChunks.push(attribute.nodeValue);
}
}
}
if (n.nodeType == n.TEXT_NODE && n.parentNode.tagName !== "SCRIPT" && n.parentNode.nodeName !== "STYLE") {
// we found a templatable text node
foundChunks.push(n.textContent);
} else {
___collectTemplatableTextChunks(n, foundChunks);
}
}
return foundChunks;
}
let ___domEventAttributeNames = new Set([
...Object.getOwnPropertyNames(document),
...Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document))),
...Object.getOwnPropertyNames(Object.getPrototypeOf(window)),
].filter(k => k.startsWith("on") && (document[k] == null || typeof document[k] == "function")));
function ___isDomEventAttributeName(name) {
if(___domEventAttributeNames.has(name)) return true;
else return false;
}
function ___isTemplatableAttributeName(name) {
// note that this *doesn't* mean that they're ignored in *generated* html (i.e. generated by perchance code)
// because all of thosse strings are fully executed before being output.
if(___isDomEventAttributeName(name)) { return false; }
// i was previously ignoring data attributes and stuff, but I see no reason for that.
// sure, someone may need to use special characters in their web component attirbutes or whatever
// but they can just escape them.
return true;
}
function ___addAttributeTemplateToEl(el) {
let templatableNodes = [];
for(let attribute of Array.from(el.attributes)) {
if(!___isTemplatableAttributeName(attribute.name)) { continue; }
templatableNodes.push({el, attribute, type:"attribute"});
}
let templatedNodes = [];
for(let item of templatableNodes) {
let {attribute, el} = item;
let exprArr = __splitTextAtAllBlocks(attribute.nodeValue);
if(!exprArr[0]) { continue; }
if(exprArr && (exprArr.length > 1 || exprArr[0][0] === "[" || exprArr[0][0] === "{")) {
item.text = attribute.nodeValue;
templatedNodes.push(item);
} else {
// this attribute is just plain text - no template needed. just need to remove any escaping-backslashes
attribute.nodeValue = __processEscapedCharacters(attribute.nodeValue);
}
}
return templatedNodes;
}
function ___reAttachDomElementEventsWithRoot(el) {
if(el.___alreadyAttatchedEventsWithRoot) return;
for(let attr of Array.from(el.attributes)) {
if(___isDomEventAttributeName(attr.name)) {
let fn = el[attr.name];
el.removeAttribute(attr.name);
// el.addEventListener(attr.name.substr(2), function(event) { // <-- bad because it removes ability to write e.g. el.onchange(), and also prevents *overwriting* of the handler.
el[attr.name] = function(event) {
try {
let fnStr = fn.toString();
//fnStr = fnStr.replace("{","{ with(window.root.___proxyTarget.obj) {") + "}";
fnStr = fnStr.replace("{","{ with(window.root) {") + "}";
eval(`(${fnStr}).bind(this)(event)`);
} catch(e) {
__perchanceError(`There was an error in the <b>${attr.name}</b> attribute of this element in your HTML panel: <code>${this.outerHTML.replace(/<(.+?)>/g, "&lt;$1&gt;")}</code>. Here's the error: <span style='color:red'>${e}</span>`);
}
};
}
}
el.___alreadyAttatchedEventsWithRoot = true;
}
function update(selectorOrEl) {
// if it has been a few seconds since the error was shown, and error model isn't currently open, we can probably hide the error box so it doesn't clutter the generator UI for end-users. The dev would have clicked the error modal button before randomizing again.
try {
if(window.__lastPerchanceErrorTime && Date.now()-window.__lastPerchanceErrorTime > 1000*5 && document.querySelector("#perchance-error-container").offsetHeight === 0) {
__clearPerchanceErrors();
}
} catch(e) { console.error(e); }
// _updateOutput essentially reloads the page, whereas `update` just replaces all values of the expressions
___updateTemplatedNodes(selectorOrEl); // goes through templatedNodes array *in order* (that's important)
}
function ___htmlToElements(html) {
var template = document.createElement('template');
template.innerHTML = html;
return Array.from(template.content.childNodes);
}
let ___trackedNodesCreatedByTemplates = new Set();
try { // putting this in try/catch because it's new code and I don't want it to break anything
// if an element that was created by a square/curly block is replaced with another element *specifically using the replaceWith function*
// then we want our tracking code to treat them as the 'same element' in terms of the ___updateTemplatedNodes process.
// we don't do that for other ways of replacing elements (e.g. setting innerHTML) because there are valid use cases for that (e.g. comments plugin uses it to avoid update() causing comments iframe to refresh)
const ___originalReplaceWith = Element.prototype.replaceWith;
Element.prototype.replaceWith = function(...args) {
// check if this is a node that was created by a template
if(___trackedNodesCreatedByTemplates.has(this)) {
for(let i = 0; i < args.length; i++) { // we convert any strings/numbers/booleans since replaceWith does that automatically, but we need to get a reference to the created nodes, so we do it manually
if(typeof args[i] === "string" || typeof args[i] === "number" || typeof args[i] === "boolean") {
args[i] = document.createTextNode(args[i].toString());
}
}
// if so, then we need to swap the new node with the old one in the oldNodes array of each ___templatedNodes item:
for(let n of ___templatedNodes) {
if(n.oldNodes.includes(this)) {
n.oldNodes.splice(n.oldNodes.indexOf(this), 1, ...args);
}
}
}
___originalReplaceWith.apply(this, args);
};
} catch(e) {
console.error(e);
}
async function ___updateTemplatedNodes(selectorOrEl="") {
let container;
if(typeof selectorOrEl === 'string') {
container = document.querySelector("#output-container "+selectorOrEl);
} else {
container = selectorOrEl;
}
if(container instanceof HTMLCollection) { // i.e. they have multiple elements with the same id and they referred to it using named access: https://html.spec.whatwg.org/multipage/window-object.html#named-access-on-the-window-object
__perchanceError(`It looks like you've got multiple elements in your HTML with <b>id=${container[0].id}</b>. If you can't find any duplicates, make sure that if you've imported some HTML, the ids used in that HTML aren't the same as the ids that you're using.`);
return;
}
___trackedNodesCreatedByTemplates.clear();
// goes through attributes and text nodes in the order they were declared. (order of execution is important)
for(let n of ___templatedNodes) {
// NOTE THAT `n` IS NOT AN ACTUAL `Node` object - it's just an obj we created with similarly names properties which "represents" the original node (which we delete and replace)
// NOTE: we can't replace spaces with &nbsp or new lines with <br/> because they could (for example) have spaces in attributes that are generated, or (for example) in html tags that they generate. we don't want <a&nbsphref="http://...">...</a>
if(n.type === "textNode" && (container.contains(n.parentNode) || container === n.parentNode)) {
// remove old nodes
for(let node of n.oldNodes) {
try { n.parentNode.removeChild(node); } catch(e) { console.log(e); } // we try/catch because the node may not be in the parent anymore (e.g. it may have been deleted or moved with some javascript)
}
// create new nodes
let text = __processEscapedCharacters( __evaluateText(window.root, window.root, n.text, {declarationLineNumber:null}) );
let newNodes = ___htmlToElements(text);
// These node additions will trigger our MutationObserver (that we need for monitoring "dynamically added" nodes (i.e. nodes added with "raw" javascript) to the page so we can render them)
// so we need to mark them all as "already rendered" (because they could have originally-escaped curly/square blocks which would then be seen as unescaped ones if they were "rendered" again)
let allNewTextNodes = newNodes.filter(n => n.nodeType === Node.TEXT_NODE);
for(let newNode of newNodes) {
___getAllTextNodeDescendents(newNode, allNewTextNodes); // this function adds the text nodes to the `allTextNodes` array
}
for(let textNode of allNewTextNodes) {
textNode.___alreadyRenderedPerchanceCode = true;
}
// insert new nodes
for(let node of newNodes) {
// add it before the next sibling (if there is no next sibling, then it's the last one, so we just append)
if(n.nextSibling && n.parentNode.contains(n.nextSibling)) n.parentNode.insertBefore(node, n.nextSibling);
else n.parentNode.appendChild(node);
___trackedNodesCreatedByTemplates.add(node);
}
n.oldNodes = newNodes;
// execute script tags:
for(let i = 0; i < newNodes.length; i++) {
if(newNodes[i].tagName && newNodes[i].tagName.toLowerCase() === "script") {
newNodes[i] = await ___executeScriptTag(newNodes[i]);
}
}
} else if(n.type === "attribute" && (container.contains(n.el) || container === n.el)) {
let result = __processEscapedCharacters( __evaluateText(window.root, window.root, n.text, {declarationLineNumber:null}) );
n.attribute.nodeValue = result;
if(n.attribute.nodeName === "value") n.el.value = result; // <-- because of this: https://stackoverflow.com/a/7986111/11950764 (for input elements, `n.attribute.nodeValue` is the *default* value - so changing it doesn't update the *live* value)
n.attribute.___alreadyRenderedPerchanceCode = true; // <-- editing attributes triggers our MutationObserver (which watches for and processes "dynamic" JavaScript DOM additions) but we don't want to run rendering twice, so we do this.
} else {
// this node isn't within the container/selector that was pased to the `update()` function
}
if(n.type !== "attribute" && n.type !== "textNode") __perchanceError(`There was an invalid node type when trying to update the HTML. If you can, please post a bug report with a link to your generator on <a href="https://lemmy.world/c/perchance">lemmy.world/c/perchance</a> to help me fix this bug. Thanks!`);
}
}
window.addEventListener('message', async function (e) {
___postMessagesRecieved++;
let origin = e.origin || e.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
//console.log("Change origin when you put it on a domain!");
//console.log(origin)
if(/*origin !== "http://app.dev:3001" && */origin !== "https://perchance.org") {
//console.error("invalid origin");
return;
}
// if(e.data.command === "updateMetadataIfNeeded") {
// ___updateGeneratorMetaData();
// }
if(e.data.command === "updateOutput") {
___updateOutputMessageHandler(e);
}
if(e.data.command === "evaluateText") {
let text, callerId = e.data.callerId;
if(window.root === null) {
text = "There is an error in your generator's script.";
} else {
text = __processEscapedCharacters( __evaluateText(window.root, window.root, e.data.text, {declarationLineNumber:null}) );
}
e.source.postMessage({type:"evaluateTextResponse", text, callerId}, e.origin);
}
if(!___alreadyAttachedSaveHandler) {
// manually bubble up save commands out of this iframe and into editor
document.addEventListener("keydown", (keyEvent) => {
if (keyEvent.keyCode == 83 && (navigator.platform.match("Mac") ? keyEvent.metaKey : keyEvent.ctrlKey)) {
keyEvent.preventDefault();
e.source.postMessage({type:"saveKeyboardShortcut"}, e.origin);
}
}, false);
___alreadyAttachedSaveHandler = true;
}
});
async function ___updateOutputMessageHandler(e) {
window.codeWarningsArray = [];
__clearPerchanceErrors();
document.querySelector("#perchance-dep-load-indicator").style.display = "none";
try {
let unfoundDeps = e.data.dependencies.filter(d => d.found === false);
if(unfoundDeps.length > 0) {
__perchanceError(`${unfoundDeps.length > 1 ? "Some dependencies" : "A dependency"} (imported generator${unfoundDeps.length > 1 ? "s" : ""}) that you tried to import into your generator could not be found: <b>${unfoundDeps.map(d => d.name).join("</b>, <b>")}</b>. You can check that whether or not they exist by going to: <i>perchance.org/NAME</i> where "NAME" should be replaced with the name of the generator that you're trying to import.`);
}
if(!Array.isArray(e.data.dependencies)) {
__perchanceError(`Something went wrong with the dependencies (imported modules): ${e.data.dependencies}. This is an unusual bug and may indicate a problem with the Perchance engine. If you could post a bug report on the forum that'd be great: <a href='https://lemmy.world/c/perchance'>lemmy.world/c/perchance<a>`);
}
let foundDeps = e.data.dependencies.filter(d => d.found !== false);
for(let dep of foundDeps) {
dep.hasImportedPreprocessor = /\n\$preprocess *= *{import:/.test("\n"+dep.modelText);
}
foundDeps.sort((a,b) => a.hasImportedPreprocessor ? 1 : -1); // compile generators that rely on preprocessors last so that their preprocessor has already been compiled and is thus available in the moduleSpace when we're compiling that generator
for(let dep of foundDeps) {
if(moduleSpace[dep.name]) continue; // Don't want to recompile things that we've already got! (only a problem if there is "recursive" dependency stuff I think)
moduleSpace[dep.name] = __createPerchanceTree(dep.modelText, dep.name);
moduleSpace[dep.name].$moduleSpace = moduleSpace;
}
// get all imports in HTML and add them to window.root.$imports
let htmlTestDiv = document.createElement("div");
htmlTestDiv.innerHTML = e.data.generator.outputTemplate;
let templatableTextChunks = ___collectTemplatableTextChunks(htmlTestDiv);
let htmlImports = [];
__ignorePerchanceErrors = true;
for(let chunk of templatableTextChunks) {
htmlImports.push( ...__collectImportedModuleNamesFromText(chunk) );
}
__ignorePerchanceErrors = false;
try {
window.root = __createPerchanceTree(e.data.generator.modelText, e.data.generator.name);
} catch(e) {
console.error("Critical error in __createPerchanceTree:", e); // need to catch this else we can't e.g. tell parent that it failed to load (which is relevant for the saving process)
}
if(!window.root) {
if(e.data.generator.modelText.startsWith("$preprocess") || e.data.generator.modelText.includes("\n$preprocess")) {
let imports = e.data.generator.modelText.split("\n").map(l => __collectImportedModuleNamesFromText(l)).flat();
imports.push(...new Set(htmlImports));
imports = imports.filter(name => name !== e.data.generator.name);
__parentWindow.postMessage({type:"importsUpdate", imports:[...new Set(imports)]}, "https://perchance.org");
}
__perchanceError(`Your generator's script seems to have errors in it. If you haven't recieved any other errors above this one which could indicate what went wrong, then this could be a bug in the Perchance engine. In that case, it would be great if you could post a quick bug report on the forum: <a href="https://lemmy.world/c/perchance">lemmy.world/c/perchance<a>`);
__parentWindow.postMessage({type:"failedToLoadDueToGeneratorErrors"}, "https://perchance.org");
return;
}
window.root.$imports.push(...new Set(htmlImports));
// remove import of THIS module (most likely mistake by user)
window.root.$imports = window.root.$imports.filter(name => name !== e.data.generator.name);
moduleSpace[e.data.generator.name] = window.root;
window.root.$moduleSpace = moduleSpace;
// EDIT: This is no longer needed since we do it all in the `key=value` getter - see MARKER:odj29hfi3j0d2kj0hx24f
// if((document.referrer+"").includes("test-import-linking-removal")) {
// // do nothing
// } else {
// // tie all direct import assignments to the actual module's root node (otherwise we can't access sub-properties):
// for(let moduleRoot of Object.values(window.root.$moduleSpace)) {
// for(let node of moduleRoot.$allNodes) {
// if(node.$nodeType === "value" && typeof node.$value === "string") {
// let match = /^\{import:([a-z0-9\-]+)\}$/.exec(node.$value);
// if(match) {
// let moduleName = match[1];
// let importedModuleRoot = window.root.$moduleSpace[moduleName];
// if(importedModuleRoot) {
// if("$output" in importedModuleRoot.getSelf) {
// if(importedModuleRoot.getSelf.$valueChildren.includes("$output")) {
// let outputNode = importedModuleRoot.getSelf.$allNodes.find(n => n.$key === "$output" && n.$parent === importedModuleRoot.getSelf);
// let value;
// if(typeof outputNode.$value === "string") value = outputNode;
// else value = outputNode.$value;
// Object.defineProperty(node.$parent, node.$key, {value:value, writable:true, configurable:true}); // <-- need outputNode.$value rather than importedModuleRoot.getSelf.$output because $output will be getter if it's a value node that's not a direct link to another list (ctrl+f "tie direct references to" in createPerchanceTree.js), and so if $output=[listOfLists.selectOne], this import statement would get tied to a *specific* list in listOfLists, which is wrong. Bug "report": reddit.com/r/perchance/comments/fft71g/has_anyone_made_an_object_generator/fk0pw5d
// } else {
// Object.defineProperty(node.$parent, node.$key, {value:importedModuleRoot.getSelf.$output, writable:true, configurable:true});
// }
// } else {
// //node.$parent[node.$key] = importedModuleRoot;
// Object.defineProperty(node.$parent, node.$key, {value:importedModuleRoot.getSelf, writable:true, configurable:true});
// }
// } else {
// __perchanceError(`A generator that you imported, '${moduleName}', could not be found. If you're sure that generator exists, try saving your generator and reloading.`);
// }
// }
// }
// }
// }
// }
let currentDepNames = e.data.dependencies.map(d => d.name);
let newDepNames = window.root.$imports.filter(name => !currentDepNames.includes(name));
// // only send import updates if there are new imports
if(newDepNames.length > 0) {
// NOTE: see note below about race condition - we're always sending importsUpdate now
//e.source.postMessage({type:"importsUpdate", imports:window.root.$imports}, e.origin);
document.querySelector("#perchance-dep-load-indicator").style.display = 'block';
//document.querySelector("#output-container").innerHTML = `Loading newly imported dependenc${newDepNames.length === 1 ? "y" : "ies"}... (${newDepNames.join(", ")})`;
} else {
await ___updateOutput(e.data.generator.outputTemplate);
}
// NOTE: due to issues with race conditions during saving (https://www.reddit.com/r/perchance/comments/cgyhey/when_i_try_to_open_the_generator_it_says_that_the/), I'm just going to *always* send back an importsUpdate (I check in the main thread/frame whether the update is actually needed)
// // tell main app which imports are being *directly* used by this generator in case we
// // can clear some no-longer-used dependencies
// let unusedOrIndirectDeps = currentDepNames.filter(n => !window.root.$imports.includes(n));
// if(unusedOrIndirectDeps.length > 0) {
// EDIT: IMPORTANT: this must come before `finishedLoadingIncludingMetaData` postMessage
__parentWindow.postMessage({type:"importsUpdate", imports:window.root.$imports}, "https://perchance.org");
// }
let metaDataUpdatePromise = ___updateGeneratorMetaData(); // no need to `await` this
try { // try/catch since this is new code. can remove later.
if(e.data.generator.outputTemplate.includes(" id=")) { // <-- just an optimisation
let topLevelVariableNames = [...window.root.getPropertyNames, ...window.root.getChildNames];
let htmlElementIds = [...document.querySelectorAll("[id]")].map(el => el.id);
let intersection = htmlElementIds.filter(value => topLevelVariableNames.includes(value));
if(intersection.length > 0) {
window.codeWarningsArray.push({lineNumber:root[intersection[0]].$declarationLineNumber, generatorName:window.generatorName, warningId:"top-level-list-name-same-as-html-element-id"});
}
}
} catch(e) {
console.error(e);
}
__parentWindow.postMessage({type:"codeWarningsUpdate", warnings:window.codeWarningsArray}, "https://perchance.org");
__parentWindow.postMessage({type:"finishedLoading"}, "https://perchance.org");
(async function() {
await metaDataUpdatePromise;
await new Promise(r => setTimeout(r, 5));
// this event is used to know when we can save the generator (with correct dependencies + metadata based on current text in editor)
__parentWindow.postMessage({type:"finishedLoadingIncludingMetaData", imports:window.root.$imports}, "https://perchance.org");
})();
isFirstUpdate = false;
} catch(error) {
//debugger;
document.querySelector("#output-container").innerHTML = e.data.generator.outputTemplate;
__perchanceError(`There was an error while trying to compile your generator. Here's the error message: <blockquote>${__escapeHTMLSpecialChars(error.stack)}</blockquote>`);
__parentWindow.postMessage({type:"finishedLoading"}, "https://perchance.org");
}
}
async function ___updateGeneratorMetaData() {
// get page title from first h1 that contains text:
let h1 = [...document.querySelectorAll("h1")].filter(el => el.textContent.trim())[0];
if(!h1) h1 = document.querySelector("h2");
let generatorTitle = h1 && h1.textContent !== "Your Generator's Title" && h1.textContent !== "Minimal Example" ? h1.textContent : window.location.pathname.slice(1).split("-").filter(function(a){return a;}).map(function(word){return word[0].toUpperCase()+word.slice(1); }).join(" ");
let generatorHTML = document.querySelector("#output-container").innerHTML;
let generatorImage = "";
let generatorDescription = "";
let generatorDynamicMetaDataFunction = null;
if(root.$meta) {
if(root.$meta.title) generatorTitle = root.$meta.title.evaluateItem;
if(root.$meta.description) generatorDescription = root.$meta.description.evaluateItem;
if(root.$meta.image) generatorImage = root.$meta.image.evaluateItem;
if(root.$meta.dynamic) {
try {
// grab the lines that are in the 'dynamic' function.
let lines = window.root.$meta.getRawListText.split("\n");
let keepLines = [];
let inFunction = false;
let functionHeaderIndentSpaces = null;
for(let line of lines) {
if(line.trim().startsWith("dynamic(") || line.trim().startsWith("async dynamic(")) {
inFunction = true;
functionHeaderIndentSpaces = line.length - line.replace(/^ +/g, "").length;
keepLines.push(line.trim().replace("dynamic(", "function dynamic(").replace("=>", " {"));
continue;
}
if(inFunction) {
let indentSpaces = line.length - line.replace(/^ +/g, "").length;
if(line.trim() && indentSpaces <= functionHeaderIndentSpaces) {
break;
}
keepLines.push(line);
}
}
generatorDynamicMetaDataFunction = keepLines.join("\n") + "\n}";
if(generatorDynamicMetaDataFunction.length > 20000) {
generatorDynamicMetaDataFunction = null;
__perchanceError("Your $meta dynamic function is too long. It must be less than 20,000 characters - you can `fetch` a remote file that you uploaded to perchance.org/upload if needed.");
}
} catch(e) {
console.error(e);
}
if(generatorDynamicMetaDataFunction) {
try {
// this would be done server-side anyway, but we do it here so that we can dynamically update the title of the current browser tab
let urlParams = Object.fromEntries([...new URL(window.location.href).searchParams].filter(e => !e[0].startsWith("utm_")));
let inputs = {urlParams};
let cacheKey = generatorDynamicMetaDataFunction + "<<<--->>>" + JSON.stringify(inputs);
let outputs;
if(!window.__dynamicMetaDataCache) window.__dynamicMetaDataCache = {}; // simple cache just so while they're editing we're not e.g. spamming a `fetch` that they have in their `dynamic` function
if(window.__dynamicMetaDataCache[cacheKey]) {
outputs = window.__dynamicMetaDataCache[cacheKey];
console.log("Used cached dynamic meta data:", outputs);
} else {
outputs = await root.$meta.dynamic(inputs);
console.log("Computed dynamic meta data:", outputs);
window.__dynamicMetaDataCache[cacheKey] = outputs;
}
if(typeof outputs.title === "string") generatorTitle = outputs.title;
if(typeof outputs.description === "string") generatorDescription = outputs.description;
if(typeof outputs.image === "string") generatorImage = outputs.image;
} catch(e) {
__perchanceError(`There was an error in your $meta dynamic function. Here's the error message: <blockquote>${__escapeHTMLSpecialChars(e.stack)}</blockquote>`);
}
}
}
let metaProps = new Set(root.$meta.$valueChildren);
metaProps.delete("title");
metaProps.delete("description");
metaProps.delete("image");
if(metaProps.size > 0) __perchanceError("Looks like you've got some invalid properties in your $meta list? The only valid properties are 'title', 'description', and 'image'.");
}
if(!navigator.webdriver) { // try to avoid Google crawler title swap bug - no idea if this will fix it.
__parentWindow.postMessage({type:"metaUpdate", title:generatorTitle, html:generatorHTML, image:generatorImage, description:generatorDescription, dynamic:generatorDynamicMetaDataFunction, _validation:{generatorName:window.generatorName}}, "https://perchance.org"); // had to use "https://perchance.org" as origin so our initial dummy update (using embedded data) works.
}
await new Promise(r => setTimeout(r, 5)); // ensure postMessage has actually been sent
}
window.addEventListener("DOMContentLoaded", function() {
let dependencies;
try { // adding try/catch here in case of future instances of this: https://lemmy.world/post/4913660
dependencies = JSON.parse( decodeURI( document.querySelector("#imported-generators").textContent ) );
} catch(e) {
if(!location.href.includes("__generatorDependenciesCacheBust")) { // try a cache bust if we haven't already.
let url = new URL(window.location.href);
url.searchParams.set("__generatorDependenciesCacheBust", Math.random());
window.location.href = url.href;
return;
} else { // if we've already tried bust, don't do it again, else it'll be a refresh loop
dependencies = [];
let errorCode = 1;
let importedGeneratorsTextContent = document.querySelector("#imported-generators").textContent;
if(importedGeneratorsTextContent === "") {
errorCode = 2;
} else {
errorCode = 3;
try {
decodeURI(importedGeneratorsTextContent);
} catch(e) {
errorCode = 4;
}
}
console.error(e);
console.error("Error parsing dependencies in embed.", errorCode);
}
}
if(/__initWithDataFromParentWindow=[1-9]/.test(window.location.href)) {
// this is so that the 'reload' button in the editor works without saving - i.e. we request the fresh, potentially-not-yet-saved-to-the-server data from the parent window.
__parentWindow.postMessage({type:"requestOutputUpdate"}, "https://perchance.org");
} else {
let fakeMessageEvent = {};
fakeMessageEvent.source = {};
fakeMessageEvent.source.postMessage = function() {};
fakeMessageEvent.data = {
generator: JSON.parse( decodeURI( document.querySelector("#preloaded-generator-data").textContent ) ),
dependencies,
};
___updateOutputMessageHandler(fakeMessageEvent);
}
});
window.addEventListener("DOMContentLoaded", function() {
var h1 = window.document.querySelector("h1");
let title;
if(h1) {
title = h1.innerText ? h1.innerText : h1.textContent; // fall back to .textContent for e.g. JSDOM which doesn't support .innerText (relevant for using JSDOM to build a hacky API)
} else {
title = window.location.pathname.slice(1).split("-").filter(function(a){return a;}).map(function(word){return word[0].toUpperCase()+word.slice(1); }).join(" ");
}
document.title = title+" ― Perchance" + (title.toLowerCase().includes("gener") ? "" : " Generator");
});</script> <style> .ldspmzjfhueifssge-ring{ display:inline-block;position:relative;width:64px;height:64px;}.ldspmzjfhueifssge-ring div{ box-sizing:border-box;display:block;position:absolute;width:51px;height:51px;margin:6px;border:6px solid #444;border-radius:50%;animation:lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;border-color:#444 transparent transparent transparent;}.ldspmzjfhueifssge-ring div:nth-child(1){ animation-delay:-0.45s;}.ldspmzjfhueifssge-ring div:nth-child(2){ animation-delay:-0.3s;}.ldspmzjfhueifssge-ring div:nth-child(3){ animation-delay:-0.15s;}@keyframes lds-ring{ 0%{ transform:rotate(0deg);}100%{ transform:rotate(360deg);}}</style> <div id="output-container"> </div> <script>// We need to make sure that when a user dynamically adds an element that has an inline event
// handler (like onclick, for example) then we make sure they can directly reference their
// top-level lists within those handlers:
let mutationCallback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
for(let node of (mutation.addedNodes || [])) { // mutation.addedNodes seems to not give all descendents - just the "top-level" nodes of each addition
if(node.nodeType === 1 && (node.tagName.toLowerCase() === 'script' || node.tagName.toLowerCase() === 'style')) {
// I'm not currently handling dynamic addition of script tags, so they won't get a `with(root) { ... }` wrapping, but I think that's fine. Might add it later if needed.
// Oh, and wouldn't they already have executed anyway?
continue;
}
let addedNodesArr = [node, ...___getAllDescendentNodesIncludingTextNodes(node)]; // <-- ignores script and style nodes
for(let addedNode of addedNodesArr) {
if(addedNode.nodeType === Node.ELEMENT_NODE) {
// See below comment where I attach this handler function to learn why I commented this out.
// for(let attr of [...addedNode.attributes]) {
// processAttributeMutation(attr, addedNode);
// }
___reAttachDomElementEventsWithRoot(addedNode);
}
// See below comment where I attach this handler function to learn why I commented this out.
/*else if(addedNode.nodeType === Node.TEXT_NODE && ___isRenderableDomNode(addedNode)) {
renderDynamicallyAddedTextNode(addedNode);
}*/
}
}
if(mutation.attributeName) {
// See below comment where I attach this handler function to learn why I commented this out.
//if(mutation.target.attributes[mutation.attributeName]) processAttributeMutation(mutation.target.attributes[mutation.attributeName], mutation.target);
}
}
};
// NOTE: I originally impemented automatic evaluation on DOM mutations, but then there's this problem: https://www.reddit.com/r/perchance/comments/bz9vcu/dev_fixed_a_bug_in_the_engine_today_thanks_to/erbcjr1
// Basically, the problem is that a mutation will take {<button>button1</button>|<button>button2</button>} and spread it over FIVE DOM nodes, and so we can't render it. I was originally working under that (bad) assumption that
// each mutation would sort of be "contained" within a single text/attribute node. In retrospect this whole idea is very silly. Instead, the generator maker should render their content with evaluateItem and THEN put that into the innerHTML of some element.
// EDIT: Actually, we still need to ___reAttachDomElementEventsWithRoot for the new elements! Otherwise if they dynamically add an element (e.g. by having `output.innerHTML = someHTML` within an element's onclick handler) that itself has inline event handlers,
// then those new element's handlers wouldn't be wrapped in `with(root) { ... }`. So we still need this (but I've commented out the "rendering" stuff). Here's an example that wouldn't work without this mutation stuff: https://perchance.org/t3c1hm87wv
let ___observer = new MutationObserver(mutationCallback);
___observer.observe(document.body.querySelector("#output-container"), { childList: true, subtree: true, attributes: true });
// function processAttributeMutation(attr, ownerElement) {
// if(attr.___alreadyRenderedPerchanceCode) return; // <-- modifying the attribute will trigger the MutationObserver, and so to prevent an infinite loop, we do this.
//
// if(___isDomEventAttributeName(attr.name.slice(0, -1))) {
// if(attr.name.endsWith("\\")) {
// ownerElement.setAttribute(attr.name.slice(0, -1), processEscapedCharacters(attr.nodeValue) );
// ownerElement.removeAttribute(attr.name);
// // note that re-attaching `with(window.root){ ... }` happens after this function in the mutation handler
// }
// } else if(___isDomEventAttributeName(attr.name)) {
// // leave events (that are attached properly) alone
// } else {
// let name = attr.name;
// let value = attr.nodeValue;
// if(attr.name.endsWith("\\")) {
// name = attr.name.slice(0, -1);
// }
// ownerElement.removeAttribute(attr.name);
// let newValue = processEscapedCharacters( __evaluateText(window.root, window.root, value, {declarationLineNumber:null}) );
// ownerElement.setAttribute(name, newValue);
// ownerElement.attributes[name].___alreadyRenderedPerchanceCode = true;
// }
//
// attr.___alreadyRenderedPerchanceCode = true;
// }
// // If a user adds some text like "[animal]" to the document with some custom JavaScript, then we should
// // render that into "mouse" of whatever. If they didn't want that, then they need to escape special characters
// // with backslashes. Realisation that this is needed: https://www.reddit.com/r/perchance/comments/bpehfa/tip_heres_a_way_to_write_nicely_indented_html/er4ugxz/
// function renderDynamicallyAddedTextNode(textNode) {
// if(textNode.___alreadyRenderedPerchanceCode) return; // this prop is set on normal text node creation too (ctrl+f for ref:4378684367397)
//
// // create new nodes
// let text = processEscapedCharacters( __evaluateText(window.root, window.root, textNode.nodeValue, {declarationLineNumber:null}) );
// let newNodes = ___htmlToElements(text);
//
// // This is needed because adding these nodes will actually trigger our MutationObserver and we'd end up
// // with an infinite loop, re-rendering the text over and over.
// let allNewTextNodes = newNodes.filter(n => n.nodeType === Node.TEXT_NODE);
// for(let newNode of newNodes) {
// ___getAllTextNodeDescendents(newNode, allNewTextNodes); // this function adds the text nodes to the `allTextNodes` array
// }
// for(let textNode of allNewTextNodes) {
// textNode.___alreadyRenderedPerchanceCode = true;
// }
//
// // insert new nodes
// for(let node of newNodes) {
// if(node.nodeType === Node.TEXT_NODE) node.___alreadyRenderedPerchanceCode = true;
// // add it before the next sibling (if there is no next sibling, then it's the last one, so we just append)
// if(textNode.nextSibling && textNode.parentNode.contains(textNode.nextSibling)) textNode.parentNode.insertBefore(node, textNode.nextSibling);
// else textNode.parentNode.appendChild(node);
// }
// textNode.parentNode.removeChild(textNode);
// textNode.___alreadyRenderedPerchanceCode = true;
// }
function ___getAllDescendentNodesIncludingTextNodes(element, nodes=[]) { // but not scripts and styles...
for(let child of element.childNodes) {
if(child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() !== 'script' && child.tagName.toLowerCase() !== 'style') {
nodes.push(child);
___getAllDescendentNodesIncludingTextNodes(child, nodes);
} else if(child.nodeType === Node.TEXT_NODE) {
nodes.push(child);
}
}
return nodes;
}
function ___getAllTextNodeDescendents(element, nodes=[]) { // but not scripts and styles...
for(let child of element.childNodes) {
if(child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() !== 'script' && child.tagName.toLowerCase() !== 'style') {
___getAllDescendentNodesIncludingTextNodes(child, nodes);
} else if(child.nodeType === Node.TEXT_NODE) {
nodes.push(child);
}
}
return nodes;
}
function ___isRenderableDomNode(node) {
return node.nodeType === 3 || (node.nodeType === 1 && node.tagName.toLowerCase() !== 'script' && node.tagName.toLowerCase() !== 'style');
}</script> <div id="perchance-dep-load-indicator" style="display:none;position:fixed;bottom:8px;right:8px;background-color:#34ba22;color:white;cursor:pointer;"> <span style="display:inline-block;padding:0.5em;">🔃&#xFE0E; Loading imported generators...</span> </div> <div onclick="__showPerchanceErrorBox();" id="perchance-error-indicator" style="display:none;position:fixed;bottom:8px;right:8px;background-color:#fc5b5b;color:white;cursor:pointer;z-index:1000000000;"> <span style="display:inline-block;padding:0.5em;">⚠&#xFE0E; This generator has errors (click here) ⚠&#xFE0E;</span> </div> <div id="perchance-error-container" style="display:none;color:rgb(25, 25, 25);font-family:monospace;"> <div style class="background" onclick="document.querySelector('#perchance-error-container').style.display = 'none';"></div> <div style="z-index:51;" class="outer-wrapper" onclick="if(!document.querySelector('#perchance-error-container').contains(event.target)) { document.querySelector('#perchance-error-container').style.display = 'none'; }"> <div ref="contentWrapper" class="content-wrapper"> <div class="view"> <div class="modal-body" style="max-height:350px;overflow-y:auto;padding:1em;text-align:left;"> <div id="perchance-error-stream"></div> <p style="background:#b5ffb5;padding:1em;margin-bottom:0;">If you need help with errors, please post a question to the <a href="https://lemmy.world/c/perchance">perchance community</a> along with a link to your generator and someone will take a look at it for you :) </p> </div> <div class="modal-footer"> <button class="main" style="width:100%;color:white;" onclick="document.querySelector('#perchance-error-container').style.display = 'none';">close</button> </div> </div> </div> </div> </div> <style> #perchance-error-container{ display:none;position:fixed;top:0;left:0;right:0;bottom:0;font-family:monospace;z-index:1000000001;}#perchance-error-container blockquote{ background:#eee;padding:1em;}#perchance-error-container .outer-wrapper{ margin:auto;box-sizing:border-box;max-width:90%;}#perchance-error-container .background{ z-index:-1;position:fixed;top:0;left:0;width:100%;height:100%;opacity:0.2;background:#000;transition:opacity .15s linear;}#perchance-error-container .content-wrapper{ overflow:hidden;background:#fff;height:100%;width:100%;border-radius:3px;} #perchance-error-container button, #perchance-error-container input{ height:3em;}#perchance-error-container button{ background:#eee;color:#444;width:50%;border:none;outline:none;font-size:90%;}#perchance-error-container button:hover{ background:#e1e1e1;}#perchance-error-container button.main{ background:#444;color:white;}#perchance-error-container button.main:hover{ background:#333;}#perchance-error-container span.link{ cursor:pointer;text-decoration:underline;color:#1212ff;}#perchance-error-container code{ padding:0.1em 0.3em;background:#eee;}</style> <script>let __perchanceErrorString = "";
let __maxPerchanceErrorCount = 20;
let __currentPerchanceErrorCount = 0;
window.__lastPerchanceErrorTime = null;
function __perchanceError(message, ctx={}) {
// __evaluateCurlyBlock, __getTextOddsDetails, __oddsTextToNumber
window.__lastPerchanceErrorTime = Date.now();
__currentPerchanceErrorCount++;
if(__currentPerchanceErrorCount > __maxPerchanceErrorCount) return;
message = message.replace(/#<Object>/g,"#&lt;Object&gt;");
if(__ignorePerchanceErrors) { return; } // needed to ignore errors in "secondary parses" (like the one to extract html imports)
document.querySelector("#perchance-error-indicator").style.display = "block";
let errorEl = document.querySelector("#perchance-error-stream");
let intro;
if(ctx.declarationLineNumber) {
intro = `<span style="opacity:0.5;">${ctx.moduleName && ctx.moduleName !== window.generatorName ? `There is a problem with the '<b style="color: red; font-family: inherit;">${ctx.moduleName}</b>' generator. ` : ""}An error has occurred near <b style='color:red;'>line number ${ctx.declarationLineNumber}</b>:</span>`;
} else {
intro = `<span style="opacity:0.5;">${ctx.moduleName && ctx.moduleName !== window.generatorName ? `There is a problem with the '<b style="color: red; font-family: inherit;">${ctx.moduleName}</b>' generator. ` : ""}An error has occurred somewhere in your code (in lists or HTML):</span>`;
}
__perchanceErrorString += `<div>${intro} ${message}</div><hr style="border: 1px solid #e1e1e1;"/>`;
// The following line is a hack to make it so "pause on caught errors" works for perchance errors in devtools.
// Click the function names in the "call stack" to go "back in time" and find more details about your error (ask for help on the forum if needed)
try { throw new Error(""); } catch(e) {}
}
function __clearPerchanceErrors() {
document.querySelector("#perchance-error-container").style.display = 'none';
document.querySelector("#perchance-error-indicator").style.display = "none";
__perchanceErrorString = "";
document.querySelector("#perchance-error-stream").innerHTML = "";
__currentPerchanceErrorCount = 0;
}
window.clearPerchanceErrors = __clearPerchanceErrors; // public version
window.ignorePerchanceErrors = function(cb) {
let orig = __ignorePerchanceErrors;
__ignorePerchanceErrors = true;
let result = cb();
__ignorePerchanceErrors = orig;
return result;
};
function __showPerchanceErrorBox() {
document.querySelector("#perchance-error-stream").innerHTML = __perchanceErrorString;
// to override user styles on the page:
document.querySelectorAll("#perchance-error-container *").forEach(el => {
el.style.cssText += ";font-family:inherit;";
if(!el.style.cssText.includes("color")) el.style.cssText += ";color:inherit;";
});
document.querySelector("#perchance-error-container").style.display = 'flex';
}</script> <script>(async function() {
if((window.location.host === "null.perchance.org" || window.location.host === `${window.generatorPublicId}.perchance.org`) && !navigator["\u0077\u0065\u0062\u0064\u0072\u0069\u0076\u0065\u0072"]) {
while(1) {
await new Promise(r => { ["mouseenter", "mousemove", "touchstart"].forEach(n => document.addEventListener(n, r, { once: true })); });
while(document.visibilityState !== "visible" || !document.hasFocus()) await new Promise(r => setTimeout(r, 1000));
await new Promise(r => setTimeout(r, 1000*90));
fetch(`https://perchance.org/api/cv?generatorName=${window.location.pathname.slice(1)}&isFromEmbed=1&__cacheBust=${Math.random()}`, {mode:"no-cors"}).catch(e => console.error(e));
await new Promise(r => setTimeout(r, 1000*60*60*6));
}
}
})();</script> <script>(function() {
if(window.location.host !== "null.perchance.org" && window.location.host !== `${window.generatorPublicId}.perchance.org`) return;
let alreadySentSignal = false; // just to be sure
let interval;
let alreadyInitedFocusBugFix = false;
function initFocusBugFix() {
if(alreadyInitedFocusBugFix) return;
alreadyInitedFocusBugFix = true;
// This is to help fix the ad-related focus bug, but it's harmless to add anyway - just makes it so focus is "recovered" to the last active element, rather than the body element.
let lastActiveElementDuringFocus = null;
setInterval(() => {
if(document.hasFocus()) lastActiveElementDuringFocus = document.activeElement;
}, 1000);
window.addEventListener("focus", function() {
if(lastActiveElementDuringFocus) {
// only if it was a text input element:
const lastActiveWasTextInput = (lastActiveElementDuringFocus.nodeName === "TEXTAREA" || (lastActiveElementDuringFocus.nodeName === "INPUT" && lastActiveElementDuringFocus.type === "text"));
if(lastActiveWasTextInput) {
lastActiveElementDuringFocus.focus();
lastActiveElementDuringFocus.click();
}
}
});
}
let messageType = "usingAdPoweredPlugin";
// for embeds within embeds, we pass the message up the chain:
window.addEventListener("message", function(e) {
if(e.data.type === messageType) {
__parentWindow.postMessage({type:messageType}, "*"); // must use wildcard origin because could be embed within embed
}
});
function detectAdPoweredPlugin() {
if(!alreadySentSignal) {
if(window.root && window.root.$moduleSpace && (window.root.$moduleSpace["text-to-image-plugin"] || window.root.$moduleSpace["ai-text-plugin"])) {
__parentWindow.postMessage({type:messageType}, "*"); // must use wildcard origin because could be embed within embed
initFocusBugFix();
clearInterval(interval);
alreadySentSignal = true;
return;
}
if([...document.querySelectorAll("iframe")].filter(el => el.src.startsWith("https://image-generation.perchance.org/embed") || el.src.startsWith("https://text-generation.perchance.org/embed")).length > 0) {
__parentWindow.postMessage({type:messageType}, "*"); // must use wildcard origin because could be embed within embed
initFocusBugFix();
clearInterval(interval);
alreadySentSignal = true;
return;
}
}
}
interval = setInterval(detectAdPoweredPlugin, 1000);
setTimeout(() => {
clearInterval(interval);
if(!alreadySentSignal) {
interval = setInterval(detectAdPoweredPlugin, 10000);
setTimeout(() => clearInterval(interval), 2*60*1000);
}
}, 10000);
})();</script> <script>(async function() {
// Here we tell the server to update the cache if needed, and if it did, then we temporarily bust the cache using a query string.
if(window.location.host === "null.perchance.org" || window.location.host === `${window.generatorPublicId}.perchance.org`) { // only if this HTML file is hosted at null.perchance.org / <publidId>.perchance.org
if(navigator.webdriver) return; // just in case this is what's confusing the Google crawler - RE the title bug
if(window.location.href.includes("__cacheBust")) return; // no need to run this check if cache is already busted
if(/__initWithDataFromParentWindow=[1-9]/.test(window.location.href)) return; // data is being loaded from parent
let imports = JSON.parse(decodeURI(document.querySelector("#imported-generator-names").textContent));
let clientHtmlServerRenderTime = Number(document.querySelector("#this-html-server-render-time").textContent);
let weHaveAnOldVersionOfThisGen = await fetch(`https://perchance.org/api/clearCacheIfGeneratorOrImportsHaveBeenUpdated?generatorName=${window.location.pathname.slice(1)}&importedGeneratorNames=${imports.join(",")}&clientHtmlServerRenderTime=${clientHtmlServerRenderTime}`).then(r => r.json());
if(weHaveAnOldVersionOfThisGen) {
let url = new URL(window.location.href);
url.searchParams.set("__cacheBust", Math.random());
window.location.href = url.href;
}
}
})();
if(location.href.includes("__cacheBust")) {
let url = new URL(window.location.href);
url.searchParams.delete("__cacheBust");
history.replaceState({}, "", url.href);
}</script> <script>document.addEventListener("DOMContentLoaded", function() {
if(!window.location.href.startsWith("file:") || window.location.hash !== "#edit") return;
let originalHtmlText = document.documentElement.outerHTML; // for editing + saving later
let preloadedGeneratorDataEl = document.querySelector("#preloaded-generator-data");
let preloadedGeneratorData = JSON.parse(decodeURI(preloadedGeneratorDataEl.textContent));
let importedGeneratorsEl = document.querySelector("#imported-generators");
let importedGenerators = JSON.parse(decodeURI(importedGeneratorsEl.textContent));
let importedGeneratorNamesEl = document.querySelector("#imported-generator-names");
let importedGeneratorNames = JSON.parse(decodeURI(importedGeneratorNamesEl.textContent));
function saveTextFile(text, name) {
let el = document.createElement("a");
let extension = name.split(".").pop();
let type = extension === "txt" ? "plain" : extension;
let file = new Blob([text], {type:"text/"+extension});
let url = URL.createObjectURL(file);
el.href = url;
el.download = name;
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
function makeElementDraggable(el) { // This function expects `el` to contain an element with the `drag-handle` class.
let endX = 0, endY = 0, startX = 0, startY = 0;
let dragHandleEl = el.querySelector(".drag-handle");
dragHandleEl.addEventListener("mousedown", dragMouseDown);
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
startX = e.clientX;
startY = e.clientY;
document.addEventListener("mouseup", closeDragElement);
document.addEventListener("mousemove", elementDrag);
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
endX = startX - e.clientX;
endY = startY - e.clientY;
startX = e.clientX;
startY = e.clientY;
// set the element's new position:
el.style.top = (el.offsetTop - endY) + "px";
el.style.left = (el.offsetLeft - endX) + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.removeEventListener("mouseup", closeDragElement);
document.removeEventListener("mousemove", elementDrag);
}
}
let editorCtn = document.createElement("div");
editorCtn.style.cssText = "width:700px; height:500px; position:fixed; display: flex; flex-direction: column;";
editorCtn.innerHTML = `
<div class="header" style="display:flex; overflow:auto;">
<!-- <div class="drag-handle" style=" display: flex; background: #e8e8e8; padding: 1rem;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width: 2rem;"><path d="M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h32v96H128V192c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V288h96v96H192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8H288V288h96v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6v32H288V128h32c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z"></path></svg></div> -->
<button class="drag-handle">drag</button>
<button class="save-button">save</button>
<div style="">
<div class="tabs" style="display:flex; width:max-content;">
<!-- tabs get inserted here -->
</div>
</div>
</div>
<div class="body" style="flex-grow:1;">
<!-- text editors get inserted here -->
</div>
`;
makeElementDraggable(editorCtn);
let editors = [];
editors.push({
name: preloadedGeneratorData.name,
content: preloadedGeneratorData.modelText,
main: true,
type: "lists",
visible: true,
});
editors.push({
name: preloadedGeneratorData.name,
content: preloadedGeneratorData.outputTemplate,
main: true,
type: "html",
visible: false,
});
for(let gen of importedGenerators) {
editors.push({
name: gen.name,
content: gen.modelText,
visible: false,
});
}
function saveGenerator() {
let preloadedGeneratorData_new = JSON.parse(JSON.stringify(preloadedGeneratorData));
preloadedGeneratorData_new.modelText = editors.filter(e => e.main && e.type=="lists")[0].textArea.value;
preloadedGeneratorData_new.outputTemplate = editors.filter(e => e.main && e.type=="html")[0].textArea.value;
let importedGenerators_new = JSON.parse(JSON.stringify(importedGenerators));
for(let editor of editors) {
if(editor.main) continue;
importedGenerators_new.filter(e => e.name===editor.name)[0].modelText = editor.textArea.value;
}
let text = originalHtmlText;
text = text.replace(/(<script id="preloaded-generator-data"[^>]*>)(.+?)(<\/script>)/, `$1${encodeURI(JSON.stringify(preloadedGeneratorData_new))}$3`);
text = text.replace(/(<script id="imported-generators"[^>]*>)(.+?)(<\/script>)/, `$1${encodeURI(JSON.stringify(importedGenerators_new))}$3`);
// TODO: Add `imported-generator-names` when you've added the ability to add more generators.
saveTextFile(text, preloadedGeneratorData_new.name+".html");
};
editorCtn.querySelector(".save-button").onclick = saveGenerator;
document.addEventListener("keydown", function(e) {
if(e.key === 's' && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
saveGenerator();
}
}, false);
for(let editor of editors) {
let textArea = document.createElement("textarea");
textArea.style.cssText = `width:100%; height:100%; display:${editor.visible ? "block" : "none"}; white-space:pre; font-family:monospace; tab-size:2;`;
textArea.value = editor.content;
editorCtn.querySelector(".body").appendChild(textArea);
editor.textArea = textArea;
// TODO: Handle resizing of the editor properly:
// new ResizeObserver(resizeHandler).observe(textArea)
let tabButton = document.createElement("button");
tabButton.innerHTML = `${editor.name}${editor.type=="html" ? " (html)" : ""}`;
tabButton.style.cssText = `width:max-content; ${editor.main ? "font-weight:bold;" : ""} ${editor.visible ? "color:green;" : ""}`;
tabButton.onclick = () => {
editors.forEach(e => e.textArea.style.display="none");
editors.forEach(e => e.tabButton.style.color="");
textArea.style.display = "block";
tabButton.style.color = "green";
};
editorCtn.querySelector(".tabs").appendChild(tabButton);
editor.tabButton = tabButton;
}
document.body.appendChild(editorCtn);
});</script> <script>// this is for transferring data across to the new generator-specific subdomains.
// it's no longer enabled by default - you need to add ?transferDataFromOldSubdomain to the end of the generator's URL.
// this code can probably be removed - just leaving it here for a few more months in case of forum posts asking about old data .
(async function() {
if(localStorage[`___subdomainLocalStorageAlreadyTransferred`] || !(document.referrer+"").includes("transferDataFromOldSubdomain")) {
return;
}
// this is for the embedded null.perchance.org subdomain that we create
if(window.location.host === "null.perchance.org") {
let alreadyResponded = false;
window.addEventListener('message', async function (e) {
if(e.origin !== `https://${window.generatorPublicId}.perchance.org`) return;
if(e.data.generatorName !== window.generatorName) return;
if(e.data.message !== "pls gib localstorage") return;
if(alreadyResponded) return;
alreadyResponded = true;
console.log("null subdomain received request for localstorage from publicid subdomain");
// reply with stringified localStorage data:
e.source.postMessage({generatorName:window.generatorName, localStorage:JSON.stringify(localStorage)}, e.origin);
});
}
// transfer across localStorage from null.perchance.org if this is the first time this was loaded
if(window.location.host.endsWith(".perchance.org") && window.location.host !== "null.perchance.org") {
// add a null.perchance.org iframe to the page and listen for a message from it
let iframe = document.createElement("iframe");
let alreadyGotData = false;
window.addEventListener('message', async function (e) {
if(e.origin !== "https://null.perchance.org" || e.data.generatorName !== window.generatorName || !e.data.localStorage) return;
if(alreadyGotData) return;
alreadyGotData = true;
iframe.remove();
let data = JSON.parse(e.data.localStorage);
let thereWasData = false;
for(let key in data) {
localStorage.setItem(key, data[key]);
thereWasData = true;
}
localStorage[`___subdomainLocalStorageAlreadyTransferred`] = "1";
console.log("FINISHED localstorage subdomain transfer");
if(thereWasData) {
window.location.reload();
}
});
iframe.style.cssText = "opacity:0.01; position:fixed; top:-100px; left:-100px; width:5px; height:5px; pointer-events:none;";
iframe.onload = async function() {
await new Promise(r => setTimeout(r, 3000)); // just to be sure it's ready
if(!alreadyGotData) {
iframe.contentWindow.postMessage({generatorName:window.generatorName, message:"pls gib localstorage"}, "https://null.perchance.org");
console.log("ONLOAD: publicid subdomain sent request for localstorage to null subdomain");
}
};
iframe.src = `https://null.perchance.org/${window.generatorName}?doNotRedirectToPublicIdSubdomain=1`;
document.body.appendChild(iframe);
// we spam requests until it loads (since onload may fire quite late since it waits for all remote scripts/images/etc. to load)
// we have alreadyResponded and alreadyGotData so this doesn't cause any problems.
(async function() {
let i = 0;
while(i < 10) {
await new Promise(r => setTimeout(r, 500));
if(alreadyGotData) return;
iframe.contentWindow.postMessage({generatorName:window.generatorName, message:"pls gib localstorage"}, "https://null.perchance.org");
console.log(`spamin': ${i} publicid subdomain sent request for localstorage to null subdomain`);
i++;
}
})();
}
})();</script> <script defer src="https://static.cloudflareinsights.com/beacon.min.js/vef91dfe02fce4ee0ad053f6de4f175db1715022073587" integrity="sha512-sDIX0kl85v1Cl5tu4WGLZCpH/dV9OHbA4YlKCuCiMmOQIk4buzoYDZSFj+TvC71mOBLh8CDC/REgE0GX0xcbjA==" data-cf-beacon='{"rayId":"88f1084fbcd1cb05","r":1,"version":"2024.4.1","token":"fc685cb0ca0145b3acbca350b7d29943"}' crossorigin="anonymous"></script>
</body> </html>