💔 OTP Angst Generator 💔 (2024)

⚄︎perchance

👥︎community

🌈hub

📚︎tutorial

📦︎resources

🎲︎generators

⊕︎new

🔑︎login

✏️︎edit

`, "single-equals-in-dynamic-odds": `

Warning: The dynamic odds notation on line number ###lineNumber### (in ###generatorNameText### generator's code) has a single equals sign instead of a double-equals sign. A single equals sign is used to put a value into a variable - for example age = 10 means "put the value 10 into the age variable" whereas age == 10 means "is age equal to 10?".

`, "single-equals-in-if-else-condition": `

Warning: The if/else condition on line number ###lineNumber### (in ###generatorNameText### generator's code) has a single equals sign instead of a double-equals sign. A single equals sign is used to put a value into a variable - for example age = 10 means "put the value 10 into the age variable" whereas age == 10 means "is age equal to 10?".

`, "square-brackets-wrapping-variable-name-within-square-block": `

Warning: On line number ###lineNumber### (in ###generatorNameText### generator's code), you've got a square block - i.e. some square brackets with some stuff inside. Nothing wrong with that. But inside that square block it looks like you've got another square block? Whenever you're writing stuff inside a square block, you can refer to variable and list name

directly

- no need to put them inside square blocks. So instead of [[noun].pluralForm], you'd just write [noun.pluralForm], and instead of [[characterAge] + 3], you'd just write [characterAge + 3], and instead of ^[[age] < 10], you'd just write ^[age < 10], and instead of [[age] > 60 ? [oldDescription] : [youngDescription]], you'd just write [age > 60 ? oldDescription : youngDescription]. You should use normal parentheses to group calculations and conditions, like this: [(age + 1) * height] and this: [isMammal || (isReptile && isBrown)]. In fact, if you wrap list/variable names in square brackets when you're already inside a square block then this can sometimes cause all sorts of sneaky errors that are hard to "debug". Square brackets

can

"legally" be used within square brackets in some circ*mstances, but they have a special meaning (learn more about this on the examples page - especially the "Dynamic Sub-list Referencing" section). One final note: It's okay to use square brackets within square brackets if the inner square brackets are themselves within double-quotes, like so: [n == 1 ? "[prefix]er" : "[prefix]ers"].

`, "top-level-list-name-same-as-html-element-id": `

Warning: It looks like you have a list name on line number ###lineNumber### that has the same name as an element's id="..." attribute? If so, note that these will conflict with one another and potentially cause errors. For example if you had a list called animal, then you shouldn't have an element like this: <p id="animal">...</p>

`, "duplicate-top-level-list-name": `

Warning: It looks like you have a list name on line number ###lineNumber### (in ###generatorNameText### generator's code) that has the same name as an earlier list/import/variable?

`, }; this.generatorMetaData = undefined; this.toggleOutputEditorWrap = function() { let btn = this.refs.outputEditorRefButton; let state = this.outputTemplateEditor.getOption('lineWrapping'); if(state) { btn.innerText = "wrap"; } else { btn.innerText = "unwrap"; } this.outputTemplateEditor.setOption('lineWrapping', !state); }; this.toggleCodeEditorWrap = function() { let btn = this.refs.codeEditorWrapRefButton; let state = this.modelTextEditor.getOption('lineWrapping'); if(state) { btn.innerText = "wrap"; } else { btn.innerText = "unwrap"; } // record cursor position: let cursorLine = this.modelTextEditor.getCursor().line; let cursorTop = this.modelTextEditor.cursorCoords().top - this.modelTextEditor.display.wrapper.getBoundingClientRect().top; this.modelTextEditor.setOption('lineWrapping', !state); // recover scroll to cursor position: this.modelTextEditor.scrollIntoView({line:cursorLine, char:0}, cursorTop); }; this.allListsFolded = false; this.toggleCodeEditorFold = function() { let btn = this.refs.codeEditorFoldRefButton; let state = this.modelTextEditor.getOption('lineWrapping'); this.allListsFolded ? CodeMirror.commands.unfoldAll(this.modelTextEditor) : CodeMirror.commands.foldAll(this.modelTextEditor); this.allListsFolded = !this.allListsFolded; btn.innerText = this.allListsFolded ? "unfold" : "fold"; }; this.outputIframe = this.root.querySelector("#output iframe"); this.refs.autoReloadCheckbox.checked = this.autoReload; window.addEventListener("message", async (e) => { let origin = e.origin || e.originalEvent.origin; if(origin !== "https://null.perchance.org" && origin !== `https://${window.generatorPublicId}.perchance.org`) return; if(e.data.type === "evaluateTextResponse" && typeof e.data.text === "string" && typeof e.data.callerId === "string") { let resolveMap = this.evaluateTextResolveMap; if(resolveMap[e.data.callerId]) { resolveMap[e.data.callerId](e.data.text); delete resolveMap[e.data.callerId]; } } if(e.data.type === "requestOutputUpdate") { // for 'hard reload' without having to first save data to server // user clicks reload button which triggers a fullRefresh, but it adds __initWithDataFromParentWindow=1 to the URL, which causes the outputIframeContent.html code to not use the data embedded in the HTML, and instead request this parent frame for the 'fresh' data await window.mostRecentFullRefreshUpdateDependenciesPromise; // so dependency stuff can load in parrallel with iframe for __initWithDataFromParentWindow fullRefresh this.updateOutputRequest(); } if(e.data.type === "saveKeyboardShortcut") { app.handleIframeSaveRequest(); } // if(e.data.type === "finishedLoading") { // window.perchanceOutputIframeFinishedFirstLoad = true; // // initialPageLoadSpinner.style.display = "none"; // outputLoadSpinner.style.display = "none"; // } if(e.data.type === "finishedLoadingIncludingMetaData" || e.data.type === "failedToLoadDueToGeneratorErrors") { if(e.data.imports) { // <-- need to check because failedToLoadDueToGeneratorErrors doesn't give us imports // we need to check if there are new imports before resolving the hard reload stuff because the saveGenerator function uses a hard reload before saving (which triggers these messages and resolves below), and we want to make sure we save the generator with all its up-to-date dependencies included. let wereNewDeps = await window.lastImportsUpdateDependencyCheckPromise; if(wereNewDeps) return; // don't want to resolve yet - the importsUpdate handler will trigger a second hard reload } for(let resolver of window.outputIframeHardReloadFinishedResolvers) { resolver(); } window.outputIframeHardReloadFinishedResolvers = []; } if(e.data.type === "importsUpdate" && Array.isArray(e.data.imports) && e.data.imports.filter(i => typeof i !== 'string').length === 0) { await window.thisIsNotAnOldCachedPagePromise; // <-- wait until we've confirmed that this is the latest version of this generator. this prevents a weird race condition where an import update will happen fast during page load and beat the cache bust, which cause an auto-save (importsUpdates can do this), and overwrites the new code with old code. // (note that the above promise won't resolve unless it's true) window.lastImportsUpdateDependencyCheckPromise = this.updateDependencies(e.data.imports); let wereNewDeps = await window.lastImportsUpdateDependencyCheckPromise; // <-- this actually downloads new dependencies (**if needed**, else returns false) if(wereNewDeps) { // we only update if there were new dependencies otherwise we will get into infinite loops // (due to the way I've handled removing dependencies (I keep the cached still)) TODO: this whole // imports-update part is terrible and needs re-writing to make it simple and understandable. // Wouldn't take toooo much work. this.updateOutput(true); // <-- this triggers iframe updateOutput function. `true` => fullRefresh, i.e. fully reload the iframe with __initWithDataFromParentWindow app.generatorEditedCallback({imports:e.data.imports}); } else if(app.data.dependencies.length > e.data.imports.length) { // if dependency was *dropped*, no need to update the output - just need to update the data: app.generatorEditedCallback({imports:e.data.imports}); } } if(e.data.type === "metaUpdate") { if(e.data._validation.generatorName !== app.data.generator.name) return; // <-- trying to stop weird Google crawler page title bug // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // WARNING WARNING WARNING WARNING WARNING WARNING WARNING: If you change this at all, make sure to do a thorough check for XSS bugs. Saved meta info must be sanitised ON THE SERVER during rendering. // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ let title = undefined; let description = undefined; let image = undefined; let dynamic = undefined; // if(typeof e.data.html === "string") { // if(e.data.html.length < 100_000 && window.shouldLiftGeneratorHtmlFromEmbed) { // window.document.querySelector("#generator-description").innerHTML = DOMPurify.sanitize(e.data.html, {ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'p', 'div', 'pre', 'button', 'i', 'b', 'a', 'ul', 'li', 'br', 'ol', 'hr'], ALLOWED_ATTR: ['href']}); // if(!window.generatorCanLinkWithRelFollow) window.document.querySelectorAll("#generator-description a").forEach(a => a.rel="nofollow"); // } // } if(typeof e.data.title === "string") { title = e.data.title.slice(0, 500); // MUST be sanitised on the server during rendering } if(typeof e.data.description === "string") { description = e.data.description.slice(0, 3000); // MUST be sanitised on the server during rendering } if(typeof e.data.image === "string") { image = e.data.image.slice(0, 500); // MUST be sanitised on the server during rendering } if(typeof e.data.dynamic === "string") { dynamic = e.data.dynamic.slice(0, 20000); } this.generatorMetaData = {title,image,description, dynamic}; } if(e.data.type === "codeWarningsUpdate") { let validWarningsCount = 0; this.refs.warningsModalOpenButton.style.display = "none"; this.refs.warningsWrapper.innerHTML = ""; let warningsHTML = ""; for(let warning of e.data.warnings) { if(typeof warning.lineNumber !== "number" || typeof warning.warningId !== "string" || /[^a-z0-9\-]/.test(warning.generatorName)) return; let warningTemplate = warnings[warning.warningId]; if(!warningTemplate) { console.error("invalid warning id?"); continue; } warningTemplate = warningTemplate.replace(/###lineNumber###/g, warning.lineNumber); let generatorNameText; if(warning.generatorName === window.location.pathname.slice(1)) generatorNameText = "this"; else generatorNameText = "the "+warning.generatorName+""; warningTemplate = warningTemplate.replace(/###generatorNameText###/g, generatorNameText); warningTemplate = warningTemplate.replace(/ \(in this<\/b> generator's code\)/g, ""); // hackily remove this, since it actually makes it more confusing for newbies I think. warningsHTML += warningTemplate; validWarningsCount++; if(validWarningsCount > 10) break; } if(validWarningsCount > 0) { this.refs.warningsModalOpenButton.style.display = "inline-block"; this.refs.warningsWrapper.innerHTML = warningsHTML; this.refs.warningsModal.querySelector(".modal-body").scrollTop = 0; } } // if(e.data.type === "requestFullscreen") { // if(this.outputIframe.requestFullscreen) this.outputIframe.requestFullscreen(); // else if(this.outputIframe.webkitRequestFullscreen) this.outputIframe.webkitRequestFullscreen(); // else if(this.outputIframe.mozRequestFullscreen) this.outputIframe.mozRequestFullscreen(); // else if(this.outputIframe.msRequestFullscreen) this.outputIframe.msRequestFullscreen(); // } // // if(e.data.type === "exitFullscreen") { // if(this.outputIframe.exitFullscreen) this.outputIframe.exitFullscreen(); // else if(this.outputIframe.webkitExitFullscreen) this.outputIframe.webkitExitFullscreen(); // else if(this.outputIframe.mozCancelFullscreen) this.outputIframe.mozCancelFullscreen(); // else if(this.outputIframe.msExitFullscreen) this.outputIframe.msExitFullscreen(); // } }); // codemirror settings CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; CodeMirror.keyMap.default["Tab"] = "indentMore"; this.root.querySelector("#input").value = this.root.querySelector("#input").value.replace(/\n[\t ]+/g, (match) => match.replace(/\\t/g, " ")); // I'm reluctant to mess with tabs in the HTML editor because they are significant in e.g.

, and I'm not sure if replacing with entity () would actually work in all cases. Should be solved for all future generators anyway (via pasting intercept code below). /*this.modelTextEditor = CodeMirror.fromTextArea(this.root.querySelector("#input"), { lineNumbers: true, foldGutter: true, extraKeys: { //"Ctrl-Q": cm => cm.foldCode(cm.getCursor()), //"Ctrl-Y": cm => CodeMirror.commands.foldAll(cm), //"Ctrl-I": cm => CodeMirror.commands.unfoldAll(cm), }, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], tabSize: 2, indentUnit: 2, indentWithTabs: false, matchBrackets: true, mode: localStorage.codeMirrorMode || "simplemode", styleActiveLine: true, //mode: "text", lineWrapping:false, //theme: "monokai", keyMap: "sublime", });*/ let systemIsInDarkMode = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); this.modelTextEditor = CodeMirror.fromTextArea(this.root.querySelector("#input"), { lineNumbers: true, foldGutter: true, extraKeys: { //"Ctrl-Q": cm => cm.foldCode(cm.getCursor()), //"Ctrl-Y": cm => CodeMirror.commands.foldAll(cm), //"Ctrl-I": cm => CodeMirror.commands.unfoldAll(cm), }, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], tabSize: 2, indentUnit: 2, indentWithTabs: false, matchBrackets: true, mode: "perchancelists", styleActiveLine: true, lineWrapping:false, theme: systemIsInDarkMode ? "one-dark" : "one-light", keyMap: "sublime", highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}, }); // hacky fix for a bug where if you type two consecutive backticks, the `perchancelists` mode's JS state gets messed up for some reason. // we just retokenize when backtick is typed. let backtickHighlightRetokenizeTimeout = null; this.modelTextEditor.on("beforeChange", function(cm, changeObj) { if(changeObj.text.some(line => line.includes('`'))) { clearTimeout(backtickHighlightRetokenizeTimeout); backtickHighlightRetokenizeTimeout = setTimeout(() => cm.setOption("mode", "perchancelists"), 200); } }); this.outputTemplateEditor = CodeMirror.fromTextArea(this.root.querySelector("#template"), { // lineNumbers: true, // foldGutter: true, // gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], mode: {name: "htmlmixed"}, selectionPointer: true, tabSize: 2, indentUnit: 2, indentWithTabs: false, lineWrapping:false, styleActiveLine: true, theme: systemIsInDarkMode ? "one-dark" : "one-light", keyMap: "sublime", highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}, }); if(localStorage.editorFontSize && !isNaN(Number(localStorage.editorFontSize))) { let size = Number(localStorage.editorFontSize); let style = document.createElement("style"); style.innerHTML = `.CodeMirror { font-size: ${size}px !important; line-height: ${size*1.4}px !important; }`; document.head.appendChild(style); } window.editsHaveBeenMadeSincePageLoad = false; // these are also called after the generator is saved, so long as window.lastEditorsChangeTime is the same as the instant that the save operation was initiated. // codemirror will make editor.isClean() return true when it comes back to this state, **including via undo** this.modelTextEditor.markClean(); this.outputTemplateEditor.markClean(); // so the save code can access them for markClean stuff: window.modelTextEditor = this.modelTextEditor; window.outputTemplateEditor = this.outputTemplateEditor; // hide wrap/fold buttons if cursor is near them, or on first line async function updateToolbarVisibility(editor) { await new Promise(r => setTimeout(r, 10)); let editorCtn = editor.getWrapperElement().parentElement; const cursor = editorCtn.querySelector(".CodeMirror-cursor"); const hoverToolbar = editorCtn.querySelector('.code-editor-buttons-ctn') || editorCtn.querySelector('.toggle-wrap-button-html'); if(!cursor || !hoverToolbar) return; const cursorRect = cursor.getBoundingClientRect(); const toolbarRect = hoverToolbar.getBoundingClientRect(); let isNear = cursorRect.top <= toolbarRect.bottom+20 && cursorRect.left >= toolbarRect.left-50 && cursorRect.left <= toolbarRect.right+50; if(editor.getCursor().line === 0) isNear = true; hoverToolbar.style.pointerEvents = isNear ? 'none' : 'auto'; hoverToolbar.style.opacity = isNear ? '0' : '1'; } window.modelTextEditor.on('keyHandled', updateToolbarVisibility); window.outputTemplateEditor.on('keyHandled', updateToolbarVisibility); window.modelTextEditor.getWrapperElement().addEventListener('mouseup', () => updateToolbarVisibility(window.modelTextEditor)); window.outputTemplateEditor.getWrapperElement().addEventListener('mouseup', () => updateToolbarVisibility(window.outputTemplateEditor)); // this is also set in the save handler. they're used as an extra check in doLocalGeneratorBackupIfNeeded to prevent backing up text that is already saved. window.lastModelTextSaved = this.modelTextEditor.getValue(); window.lastOutputTemplateSaved = this.outputTemplateEditor.getValue(); // this is stupid but it's the only way I could get chrome to stop suggesting an email/password in codemirror's search dialogue try { function watchForChildWithClass(target, className, callback) { new MutationObserver(mutations => { mutations.forEach(mutation => { Array.from(mutation.addedNodes).forEach(node => { if(node.nodeType === 1 && node.classList.contains(className)) callback(node); }); }); }).observe(target, { childList: true }); } setTimeout(() => { for(let container of Array.from(document.querySelectorAll(".CodeMirror"))) { watchForChildWithClass(container, "CodeMirror-dialog", element => { for(let inputEl of Array.from(element.querySelectorAll("input"))) { let dummyEl = document.createElement("div"); dummyEl.innerHTML = ``; dummyEl.style.cssText = "opacity:0; pointer-events:none; position:absolute;"; inputEl.parentElement.insertBefore(dummyEl, inputEl); } }); } }, 400); } catch(e) { console.error(e); } // there was some funky business happening with tabs when people *pasted* them. // This replaces all tabs with two spaces (users need to write \t if they want a tab in their text). // EDIT: also just added non-breaking space replacement with normal space, since it was messing with indenting stuff. this.modelTextEditor.on("beforeChange", (cm, change) => { if(change.origin === "paste") { change.update(null, null, change.text.map(line => line.replace(/\t/g, " ").replaceAll(`\u00A0`, " "))); } }); // I took this out because in HTML, some text with to consecutive spaces is different to , and I think the same is true of tabs. So I think this would turn code indents into a bunch of visible whitespace. // this.outputTemplateEditor.on("beforeChange", (cm, change) => { // if(change.origin === "paste") { // change.update(null, null, change.text.map(line => line.replace(/\t/g, ""))); // } // }); // This is hacky, but it seems to work. It's from here: https://codemirror.net/demo/indentwrap.html // It makes it so wrapped text is indented at the same indent level as the line itself. let basePadding = 4; // Only do the indentwrap stuff if there are no tabs (literal `\t` is fine), because otherwise it (for some reason) doesn't work - it messes things up: jsbin.com/tukuximome if(!this.root.querySelector("#template").value.includes("\t")) { let charWidthOutputTemplate = this.outputTemplateEditor.defaultCharWidth(); this.outputTemplateEditor.on("renderLine", function(cm, line, elt) { let off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidthOutputTemplate; elt.style.textIndent = "-" + off + "px"; elt.style.paddingLeft = (basePadding + off) + "px"; }); } if(!this.root.querySelector("#input").value.includes("\t")) { let charWidthModelText = this.modelTextEditor.defaultCharWidth(); this.modelTextEditor.on("renderLine", function(cm, line, elt) { let off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidthModelText; elt.style.textIndent = "-" + off + "px"; elt.style.paddingLeft = (basePadding + off) + "px"; }); } ////////////////////////////////////// // LOCAL STORAGE BACKUPS // ////////////////////////////////////// const maxLocalBackupInterval = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*5 : 1000*60*5; // CAUTION: if you change this, change `doLocalBackupDebounce6MinTimeout` to be larger than it const oldLocalBackupCleaningInterval = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*60*2 : 1000*60*20; const maxLocalBackupAge = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*60*10 : 1000*60*60*24*30; const maxLocalBackupsPerGenerator = localStorage.__speedUpLocalBackupStuffForTesting ? 10 : 30; if(localStorage.__speedUpLocalBackupStuffForTesting) { console.warn(`WARNING: local backups sped up due to truthy localStorage.__speedUpLocalBackupStuffForTesting value\n`.repeat(20)); } let lastLocalBackupTime = Date.now(); const doLocalGeneratorBackupIfNeeded = async (modelText, outputTemplate) => { if(!app.userOwnsThisGenerator) return; // they don't own this gen (i.e. are just playing around in editor for now) if(Date.now()-window.generatorLastSaveTime < maxLocalBackupInterval) return; // hasn't been a few mins since last save if(Date.now()-lastLocalBackupTime < maxLocalBackupInterval) return; // at most once every few mins if(window.lastModelTextSaved === modelText && window.lastOutputTemplateSaved === outputTemplate) return; // since they may have made an edit, but then undone it lastLocalBackupTime = Date.now(); if(!kv) await window.initKv(); let backupsArray = await kv.localBackups.get(app.data.generator.name) || []; backupsArray.unshift({modelText, outputTemplate, time:Date.now()}); // if there are more than `maxLocalBackupsPerGenerator` backups, delete every second one in the second half of the array: if(backupsArray.length > maxLocalBackupsPerGenerator) { backupsArray = backupsArray.filter((backup, i) => i < maxLocalBackupsPerGenerator/2 ? true : i%2===0); } await kv.localBackups.set(app.data.generator.name, backupsArray); console.debug("Saved local backup. NEW backupsArray:", backupsArray); }; setTimeout(async () => { if(app.userOwnsThisGenerator) { let persistent = await navigator.storage.persist(); if(!persistent) console.warn("Browser denied persistent local storage."); } }, 1000*60*20); setInterval(async () => { if(!kv) return; let entries = await kv.localBackups.entries(); // for *every* backed-up generator (not just the one they're currently viewing/editing) stored in this user's browser: for(let [generatorName, backupsArray] of entries) { let originalLength = backupsArray.length; // delete the backup array entries older than several weeks: backupsArray = backupsArray.filter(backup => Date.now()-backup.time < maxLocalBackupAge); if(originalLength === backupsArray.length) continue; // save changes: if(backupsArray.length === 0) { await kv.localBackups.delete(generatorName); await kv.localBackupsLastWarnTimes.delete(generatorName); } else { console.debug(`Cleared ${originalLength-backupsArray.length} old backups. NEW backupsArray:`, backupsArray); await kv.localBackups.set(generatorName, backupsArray); } } }, oldLocalBackupCleaningInterval); // if the page loads, and user owns this generator, and the last backup time is more than a few mins forward in time from last save, then warn them to click header 'backups' button setTimeout(async () => { let startTime = Date.now(); while(app.userOwnsThisGenerator === undefined) await new Promise(r => setTimeout(r, 1000*10)); if(Date.now()-startTime > 1000*60) { console.warn(`Took a ${Date.now()-startTime}ms (i.e. much longer than expected) to determine if this user owns this generator?`); return; } if(!app.userOwnsThisGenerator) return; if(!kv) await window.initKv(); let localBackupsArr = await kv.localBackups.get(app.data.generator.name); let lastWarnTimeForThisGen = await kv.localBackupsLastWarnTimes.get(app.data.generator.name) || 0; if(localBackupsArr) { for(let data of localBackupsArr) { if(data.time-window.generatorLastSaveTime > 1000*60*3 && data.time > lastWarnTimeForThisGen) { try { app.goToEditMode(); document.querySelector(`#menuBarEl [data-ref="revisionsButton"]`).style.backgroundColor = "#7d5100"; } catch(e) { console.error(e); } await kv.localBackupsLastWarnTimes.set(app.data.generator.name, Date.now()); alert(`Warning: There's a backup of this generator stored in your browser memory which is newer than the version of this generator you're currently viewing. This could have been caused by a problem where the server didn't properly save your generator, or if your browser previously crashed while you were editing, or it could just be that you made a little edit but then decided not to save it. If you lost some work, click the 'backups' button in the header to download the code for a backed-up version.`); return; } } } }, 1000*3); let editorContentAlreadySavedDebounceTimeout; function checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate) { clearTimeout(editorContentAlreadySavedDebounceTimeout); editorContentAlreadySavedDebounceTimeout = setTimeout(() => { if(window.lastModelTextSaved === modelText && window.lastOutputTemplateSaved === outputTemplate) { if(app.userOwnsThisGenerator) { window.setSaveState("saved"); } } }, 300); } let doLocalBackupDebounce6MinTimeout; window.lastEditorsChangeTime = Date.now(); this.outputTemplateEditor.on("changes", () => { window.lastEditorsChangeTime = Date.now(); window.editsHaveBeenMadeSincePageLoad = true; let modelText = this.modelTextEditor.getValue(); let outputTemplate = this.outputTemplateEditor.getValue(); app.generatorEditedCallback({modelText, outputTemplate}); if(this.outputTemplateEditor.isClean() && this.modelTextEditor.isClean()) { // in case they undo and go back to a saved state if(app.userOwnsThisGenerator) { window.setSaveState("saved"); // must come after generatorEditedCallback, above, since that sets the save state to "unsaved" } } else { checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate); } try { doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); clearTimeout(doLocalBackupDebounce6MinTimeout); doLocalBackupDebounce6MinTimeout = setTimeout(() => { // without this, we can still lose up to 5 minutes of work, which is annoying doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); }, 1000*60*6); } catch(e) { console.error(e); } }); this.modelTextEditor.on("changes", () => { window.lastEditorsChangeTime = Date.now(); window.editsHaveBeenMadeSincePageLoad = true; let modelText = this.modelTextEditor.getValue(); let outputTemplate = this.outputTemplateEditor.getValue(); app.generatorEditedCallback({modelText, outputTemplate}); if(this.outputTemplateEditor.isClean() && this.modelTextEditor.isClean()) { // in case they undo and go back to a saved state if(app.userOwnsThisGenerator) { window.setSaveState("saved"); // must come after generatorEditedCallback, above, since that sets the save state to "unsaved" } } else { checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate); } try { doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); clearTimeout(doLocalBackupDebounce6MinTimeout); doLocalBackupDebounce6MinTimeout = setTimeout(() => { // without this, we can still lose up to 5 minutes of work, which is annoying doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); }, 1000*60*6); } catch(e) { console.error(e); } }); // disable overwrite insert mode: this.outputTemplateEditor.on("keyup", () => { this.outputTemplateEditor.toggleOverwrite(false); }); this.modelTextEditor.on("keyup", () => { this.modelTextEditor.toggleOverwrite(false); }); this.outputTemplateEditor.on("changes", () => { clearTimeout(this.updateOutputDelayTimeout); if(this.autoReload) { this.updateOutputDelayTimeout = setTimeout(() => { this.updateOutput(false, true); }, 800); } }); this.modelTextEditor.on("changes", () => { clearTimeout(this.updateOutputDelayTimeout); if(this.autoReload) { this.updateOutputDelayTimeout = setTimeout(() => { this.updateOutput(false, true); // EDIT: this is commented out because it doesn't make sense - if they haven't reloaded, then the computed meta data is obviously going to be using the old code. // TODO: Make it so that when they send the save request, that's when we send updateMetadataIfNeeded, and we send the updated modelText which is executed with createPerchanceTree or whatever, but is then discarded, rather than replacing the "real" tree and updating everything. // this.outputIframe.contentWindow.postMessage({command:"updateMetadataIfNeeded"}, '*'); // needed because they may not have 'auto' checked, and so they make some changes to their $meta props, then hit save, but the updated $meta stuff isn't sent along with the save request. }, 800); } }); // if(window.DEBUG_FREEZE_MODE) { // this.refs.autoReloadCheckbox.checked = false; // //this.root.querySelector("#output iframe").src = "https://null.perchance.org/debug-freeze"; // } else { // // let url = "https://null.perchance.org/" + app.data.generator.name + "?v=1"; // // let url = `https://${window.generatorPublicId}.perchance.org/` + app.data.generator.name + "?v=2"; // // if(window.needToBustCacheOfIframeOnInitialLoad) url += `?__cacheBuster=${Math.random()}`; // let url = new URL(window.location.href); // url.hostname = `${window.generatorPublicId}.perchance.org`; // if(window.needToBustCacheOfIframeOnInitialLoad) { // url.searchParams.set("__cacheBust", Math.random().toString()+Math.random().toString()); // } // // This is to ensure cache is busted even if they have URL params added to the URL. // // Cloudflare has limits to the number of cache clears, so this is a hacky but full-proof workaround. // // The top-level frame has the `window.thisIsNotAnOldCachedPagePromise` stuff to handle this, but we still // // need to make sure the iframe is also busted. // // We only need this during initial page load - not when reload button / save button is pressed, since in those // // cases the __initWithDataFromParentWindow query param is added to the URL, which means the iframe gets fresh // // data from the parent, regardless of the html that the server sends (i.e. the data that's embedded in the HTML // // is ignored) // url.searchParams.set("__generatorLastEditTime", window.generatorLastSaveTime); // this.root.querySelector("#output iframe").src = url; // } if(window.DEBUG_FREEZE_MODE) { this.refs.autoReloadCheckbox.checked = false; this.root.querySelector("#output iframe").src = "https://null.perchance.org/debug-freeze"; } else if(window.needToBustCacheOfIframeOnInitialLoad) { let url = new URL(window.location.href); url.hostname = `${window.generatorPublicId}.perchance.org`; url.searchParams.set("__cacheBust", Math.random().toString()+Math.random().toString()); this.root.querySelector("#output iframe").src = url; } // whenever the codemirror scrollbar appears or dissapears, update the buttons positioning so they sit beside the scrollbar: const updateCodeEditorButtonsPos = () => { document.querySelector(".code-editor-buttons-ctn").style.right = (document.querySelector(".CodeMirror-vscrollbar").offsetWidth+5)+"px"; }; new MutationObserver(updateCodeEditorButtonsPos).observe(document.querySelector(".CodeMirror-vscrollbar"), {attributes: true}); setTimeout(updateCodeEditorButtonsPos, 2000); // <-- call it once at the start for init // same for html area: const updateHtmlEditorButtonsPos = () => { document.querySelector(".toggle-wrap-button-html").style.right = (document.querySelectorAll(".CodeMirror-vscrollbar")[1].offsetWidth+5)+"px"; }; new MutationObserver(updateHtmlEditorButtonsPos).observe(document.querySelectorAll(".CodeMirror-vscrollbar")[1], {attributes: true}); setTimeout(updateHtmlEditorButtonsPos, 2000); // <-- call it once at the start for init // Initialise Split.js panels let sizesStore = new Store('editor-split-sizes'); let sizes; let defaultSizes = { ab: [60,40], cd: [90,10], ef: [75,25], }; if(sizesStore.data[app.data.generator.name]) { sizes = sizesStore.data[app.data.generator.name]; } else { // default sizes sizes = JSON.parse(JSON.stringify(defaultSizes)); sizesStore.data[app.data.generator.name] = sizes; sizesStore.save(); } function trimSplitSizes(arr) { // for some reason split sizes sometimes add to over 100% and the editor breaks. // this trims it to 100% arr = arr.slice(0); let sum = arr[0] + arr[1]; if(sum <= 100) return arr; else { arr[0] = Math.round(arr[0]); arr[1] = Math.round(arr[1]); while(arr[0] + arr[1] > 100) { if(Math.random() < 0.5) { arr[0]--; } else { arr[1]--; } } return arr; } } let split_ab = Split([this.root.querySelector('#a'), this.root.querySelector('#b')], { gutterSize: 8, sizes: sizes.ab, cursor: 'col-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.ab = split_ab.getSizes(); data.ab = trimSplitSizes(data.ab); sizesStore.save(); } }); let split_cd = Split([this.root.querySelector('#c'), this.root.querySelector('#perchanceConsoleEl')], { direction: 'vertical', sizes: sizes.cd, gutterSize: 8, cursor: 'row-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.cd = split_cd.getSizes(); data.cd = trimSplitSizes(data.cd); sizesStore.save(); } }); let split_ef = Split([this.root.querySelector('#e'), this.root.querySelector('#f')], { direction: 'vertical', sizes: sizes.ef, gutterSize: 8, cursor: 'row-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.ef = split_ef.getSizes(); data.ef = trimSplitSizes(data.ef); sizesStore.save(); } }); // need this because otherwise for some reason there's some weird `overscroll-behaviour` stuff near the edges of the screen (even with touch-action:none overlay workaround). if(window.location.hash !== "#edit") this.root.querySelectorAll("#main .gutter").forEach(el => el.style.display="none"); setTimeout(() => { this.modelTextEditor.refresh(); this.outputTemplateEditor.refresh(); }, 10); ////////////////////// // CONSOLE // ////////////////////// function escapeHtml(html) { return html.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } let consoleOutputStyle = ""; let consoleCommandStore = new Store("console-commands"); if(!consoleCommandStore.data.generators) { consoleCommandStore.data.generators = {}; } let consoleOutputEl = document.querySelector("#console-output"); let consoleInputEl = document.querySelector("#console-input"); consoleOutputEl.addEventListener("click", function() { consoleInputEl.focus(); }); consoleOutputEl.srcdoc = consoleOutputStyle + `e.g. try typing I rolled a \{1-6\}! or [yourListName] (including the brackets) above, and then press enter.`; let previousCommands = consoleCommandStore.data.generators[app.data.generator.name] || [""]; let currCommandIndex = 0; // NOT previousCommands array index!! 0 represents most recent element, 1 is second most recent, etc. consoleInputEl.addEventListener("keydown", async (e) => { // e.preventDefault(); let command = consoleInputEl.value; if(e.which === 13) { // enter let optEl = document.querySelector("#consoleInterpreterOption"); let interpreter = optEl.options[optEl.selectedIndex].value; if(interpreter === "plaintext") { consoleOutputEl.srcdoc = consoleOutputStyle.replace("font-family:monospace;", "font-family:monospace; white-space:pre-wrap;")+(escapeHtml(await this.evaluateText(command))); } else if(interpreter === "html") { consoleOutputEl.srcdoc = consoleOutputStyle+(await this.evaluateText(command)); } // this.root.style.background = "#f1f1f1"; // setTimeout(() => { this.root.style.background = "white"; },100) let prevCommand = previousCommands[previousCommands.length-1]; if(prevCommand !== command) { previousCommands.pop(); previousCommands.push(command); previousCommands.push(""); // put the empty one back on the end } if(previousCommands.length > 1000) { previousCommands.shift(); } currCommandIndex = 1; // <-- set index to that of the command that was just submitted consoleCommandStore.data.generators[app.data.generator.name] = previousCommands; // (not necessary because it keeps reference??) consoleCommandStore.save(); return; } if(e.which === 38) { // up arrow currCommandIndex++; // move towards start of array (counter-intuitive) currCommandIndex = Math.min(previousCommands.length-1, currCommandIndex); // must stay below or at length of command stack consoleInputEl.value = previousCommands[previousCommands.length-1-currCommandIndex]; setTimeout(function() { consoleInputEl.selectionStart = consoleInputEl.selectionEnd = 100000; }, 1); return; } if(e.which === 40) { // down arrow currCommandIndex--; // move towards end off array (counter-intuitive) currCommandIndex = Math.max(0, currCommandIndex); // must stay above or at zero consoleInputEl.value = previousCommands[previousCommands.length-1-currCommandIndex]; setTimeout(function() { consoleInputEl.selectionStart = consoleInputEl.selectionEnd = 100000; }, 1); return; } }); }, }; 

💔 OTP Angst Generator 💔 (2024)

FAQs

What are some questions to ask OTP? ›

35 OTP Questions
  • Who fell for the other one first?
  • Who made the first move?
  • Describe their first date.
  • What's a perfect date for them?
  • What do physical trait do they love the most about each other?
  • What personality trait do they love the most about each other?
Jul 23, 2022

What is an example of angst dialogue? ›

“Don't pretend like you're not happy to see me like this.” “There is nothing worse than seeing you get what you want.” “Your mind must be a horrible place.” “You can cut me, bruise me and skin me alive, but you will not take her from me.”

What are 4 good questions to ask? ›

Deep Questions to Ask Someone
  • What's your favorite quality about yourself? ...
  • What is something that makes you feel unstoppable?
  • What three words would you use to describe yourself?
  • Would you call yourself brave?
  • When do you feel the safest?
  • What's your favorite childhood memory?
  • What's your biggest life regret so far?
Jul 31, 2023

What is an angst example? ›

angst | Intermediate English

a feeling of extreme anxiety and unhappiness: The boy's mysterious disappearance has caused angst and guilt for the family.

What is angst love? ›

For me it means heavy emotions. Heartbreak, sadness, etc. A book where the characters have to break up might be angsty if there's an emotional toll. Or a book with one of the characters overcoming some trauma might be angsty.

Why is angst called angst? ›

The word angst has existed in German since the 8th century, from the Proto-Indo-European root *anghu-, "restraint" from which Old High German angust developed. It is pre-cognate with the Latin angustia, "tensity, tightness" and angor, "choking, clogging"; compare to the Ancient Greek ἄγχω (ánkhō) "strangle".

What are 5 questions to ask someone? ›

Try these:
  • What is on your bucket list?
  • What are you most thankful for?
  • What is your biggest regret in life?
  • What are you most afraid of?
  • What do you feel most passionate about?
  • How do you like to spend your free time?
  • What would your perfect day be like?
  • What does your dream life look like?
Feb 29, 2024

What are some good questions questions? ›

100 Getting to Know You Questions
  • Who is your hero?
  • If you could live anywhere, where would it be?
  • What is your biggest fear?
  • What is your favorite family vacation?
  • What would you change about yourself if you could?
  • What really makes you angry?
  • What motivates you to work hard?

What questions to ask to stand out? ›

10 questions to ask in an interview to really stand out
  • “What does a typical day look like for this role?” ...
  • “What are the greatest challenges for your team right now?” ...
  • “How long have you been with the company and what is it like working there?”

What are good 21 questions? ›

Best questions to ask
  • What movie never fails to make you cry?
  • How would your best friends describe you?
  • How far would you make it in a horror movie?
  • Who in your family are you closest to?
  • How would you describe yourself in three words?
  • If you had the option to press a button and restart your life, would you press it?
Aug 20, 2024

Top Articles
A Nearly Normal Family
A Nearly Normal Family: Limited Series | Rotten Tomatoes
Laura Loomer, far-right provocateur who spread 9/11 conspiracy theory, influencing Trump as he searches for a message | CNN Politics
Ess Compass Associate Portal Login
Mimissliza01
How Much Is Vivica Fox Worth
Order Irs Tax Forms Online
Main Moon Ashland Ohio Menu
Craigslist Free En Dallas Tx
Nizhoni Massage Gun
The Blind Showtimes Near Merchants Walk Cinemas
Job Shop Hearthside Schedule
Mta Bus Time Q85
Trinket Of Advanced Weaponry
Unterschied zwischen ebay und ebay Kleinanzeigen: Tipps, Vor- und Nachteile
Green Light Auto Sales Dallas Photos
Nbl Virals Series
Nancy Pazelt Obituary
Craigslist Yamhill
San Antonio Craigslist Free
Asa Morse Farm Photos
Wwba Baseball
Ok Google Zillow
Shawn N. Mullarkey Facebook
Logisticare Transportation Provider Login
Joy Ride 2023 Showtimes Near Cinemark Huber Heights 16
Pella Culver's Flavor Of The Day
What to know about Canada and China's foreign interference row
Sotyktu Pronounce
Winsipedia
Gmc For Sale Craigslist
Lowes Light Switch
Rage Room Longmont
1875 Grams To Pounds And Ounces
Bullmastiff vs English Mastiff: How Are They Different?
Taika Waititi Birth Chart
La Monja 2 Pelicula Completa Tokyvideo
Skip The Games Albany
C And B Processing
O'reillys Parts Store
Arrival – AIRPOWER24 6th – 7th Sept 24
Intel Core i3-4130 - CM8064601483615 / BX80646I34130
11526 Lake Ave Cleveland Oh 44102
Natalya Neidhart: Assembling the BOAT
Espn Masters Leaderboard
Ark Extinction Element Vein
Www.888Tt.xyz
Craigslist Pgh Furniture
Pioneer Library Overdrive
Vci Classified Paducah
Priority Pass: How to Invite as Many Guests as Possible to Airport Lounges?
Sterling Primary Care Franklin
Latest Posts
Article information

Author: Stevie Stamm

Last Updated:

Views: 5729

Rating: 5 / 5 (60 voted)

Reviews: 91% of readers found this page helpful

Author information

Name: Stevie Stamm

Birthday: 1996-06-22

Address: Apt. 419 4200 Sipes Estate, East Delmerview, WY 05617

Phone: +342332224300

Job: Future Advertising Analyst

Hobby: Leather crafting, Puzzles, Leather crafting, scrapbook, Urban exploration, Cabaret, Skateboarding

Introduction: My name is Stevie Stamm, I am a colorful, sparkling, splendid, vast, open, hilarious, tender person who loves writing and wants to share my knowledge and understanding with you.