Hong Kong Name Generator (2024)

⚄︎perchance

👥︎community

🌈hub

📚︎tutorial

📦︎resources

🎲︎generators

⊕︎new

🔑︎login

✏️︎edit

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

Warning: The dynamic odds notation on line number (in 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 (in 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 (in 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 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 (in 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; } }); }, }; 

Hong Kong Name Generator (2024)

References

Top Articles
Everything we know about Doctor Who season 15
Where and When to Watch ‘The Little Mermaid’ Live-Action Remake
7 C's of Communication | The Effective Communication Checklist
San Angelo, Texas: eine Oase für Kunstliebhaber
Dragon Age Inquisition War Table Operations and Missions Guide
Camera instructions (NEW)
Ffxiv Palm Chippings
Ross Dress For Less Hiring Near Me
Lesson 1 Homework 5.5 Answer Key
The Haunted Drury Hotels of San Antonio’s Riverwalk
Cvs Learnet Modules
How Many Cc's Is A 96 Cubic Inch Engine
Regal Stone Pokemon Gaia
Sams Early Hours
Dr Adj Redist Cadv Prin Amex Charge
2020 Military Pay Charts – Officer & Enlisted Pay Scales (3.1% Raise)
TBM 910 | Turboprop Aircraft - DAHER TBM 960, TBM 910
2021 Volleyball Roster
Everything To Know About N Scale Model Trains - My Hobby Models
Chicago Based Pizza Chain Familiarly
The Boogeyman (Film, 2023) - MovieMeter.nl
Inter Miami Vs Fc Dallas Total Sportek
Black Panther 2 Showtimes Near Epic Theatres Of Palm Coast
R Baldurs Gate 3
TMO GRC Fortworth TX | T-Mobile Community
Guinness World Record For Longest Imessage
Rainfall Map Oklahoma
Used Safari Condo Alto R1723 For Sale
Kids and Adult Dinosaur Costume
Springfield.craigslist
Spy School Secrets - Canada's History
Strange World Showtimes Near Regal Edwards West Covina
Uhaul Park Merced
Craigslist Greencastle
Afspraak inzien
Nobodyhome.tv Reddit
Pawn Shop Open Now
SF bay area cars & trucks "chevrolet 50" - craigslist
Bismarck Mandan Mugshots
Ticket To Paradise Showtimes Near Marshall 6 Theatre
R/Moissanite
Www Usps Com Passport Scheduler
Oppenheimer Showtimes Near B&B Theatres Liberty Cinema 12
Mugshots Journal Star
Divinity: Original Sin II - How to Use the Conjurer Class
Gotrax Scooter Error Code E2
Dickdrainersx Jessica Marie
Pgecom
Mauston O'reilly's
Interminable Rooms
Aurora Southeast Recreation Center And Fieldhouse Reviews
Verizon Forum Gac Family
Latest Posts
Article information

Author: Moshe Kshlerin

Last Updated:

Views: 5913

Rating: 4.7 / 5 (57 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Moshe Kshlerin

Birthday: 1994-01-25

Address: Suite 609 315 Lupita Unions, Ronnieburgh, MI 62697

Phone: +2424755286529

Job: District Education Designer

Hobby: Yoga, Gunsmithing, Singing, 3D printing, Nordic skating, Soapmaking, Juggling

Introduction: My name is Moshe Kshlerin, I am a gleaming, attractive, outstanding, pleasant, delightful, outstanding, famous person who loves writing and wants to share my knowledge and understanding with you.