From: Lanius Trolling Date: Tue, 23 Jan 2024 17:41:14 +0000 (-0500) Subject: Add conlang vocabulary tag X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=e9e90f6711bc830ce9fd463b8a6daffebe7b2dac;p=factbooks Add conlang vocabulary tag --- diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt index a011fac..b5a4459 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt @@ -62,6 +62,7 @@ enum class TextParserFormattingTagPlainText(val type: TextParserTagType) { // Conlangs LANG(plainTextFormattingTag), ALPHABET(embeddedFormattingTag), + VOCAB(embeddedFormattingTag), ; companion object { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt index c77a3cf..0ff8cff 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt @@ -378,6 +378,16 @@ enum class TextParserFormattingTag(val type: TextParserTagType) { } else "" } ), + VOCAB( + TextParserTagType.Indirect { _, content, _ -> + if (content.isBlank()) + "" + else { + val contentJson = JsonStorageCodec.parseToJsonElement(TextParserState.uncensorText(content)) + "" + } + } + ), ; companion object { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt index d20bdff..1ca8490 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt @@ -10,6 +10,7 @@ import io.pebbletemplates.pebble.extension.Function import io.pebbletemplates.pebble.extension.escaper.EscapeFilter import io.pebbletemplates.pebble.extension.escaper.EscapingStrategy import io.pebbletemplates.pebble.extension.escaper.RawFilter +import io.pebbletemplates.pebble.extension.escaper.SafeString import io.pebbletemplates.pebble.extension.i18n.i18nFunction import io.pebbletemplates.pebble.loader.Loader import io.pebbletemplates.pebble.template.EvaluationContext @@ -60,7 +61,7 @@ object PebbleTemplateLoader : Loader { override fun resolveRelativePath(relativePath: String, anchorPath: String): String { val templateDir = File(Configuration.CurrentConfiguration.templateDir) - if ('\n' in anchorPath) // Probably a raw template string + if ('\n' in anchorPath) // Probably a raw template contents string return relativePath return templateDir.combineSafe("$anchorPath/$relativePath").toRelativeString(templateDir) @@ -204,6 +205,7 @@ object PebbleJsonLoader { fun deconvertJson(data: Any?): JsonElement = when (data) { null -> JsonNull is String -> JsonPrimitive(data) + is SafeString -> JsonPrimitive(data.toString()) is Number -> JsonPrimitive(data) is Boolean -> JsonPrimitive(data) is List<*> -> JsonArray(data.map { deconvertJson(it) }) @@ -243,7 +245,7 @@ object PebbleToJsonFilter : Filter { return mutableListOf() } - override fun apply(input: Any?, args: MutableMap?, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any { + override fun apply(input: Any?, args: MutableMap, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any? { return PebbleJsonLoader.deconvertJson(input).toString() } } @@ -253,7 +255,7 @@ object PebbleFromJsonFilter : Filter { return mutableListOf() } - override fun apply(input: Any?, args: MutableMap?, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any? { + override fun apply(input: Any?, args: MutableMap, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any? { return PebbleJsonLoader.convertJson(JsonStorageCodec.parseToJsonElement(input.toString())) } } @@ -263,7 +265,7 @@ object PebbleLoadJsonFunction : Function { return mutableListOf("data", "path") } - override fun execute(args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { + override fun execute(args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { val dataName = args["data"]?.toString() ?: throw PebbleException(null, "Missing 'data' argument", lineNumber, self.name) @@ -300,23 +302,12 @@ object PebbleScriptLoader { cache[digest] = compiledScript return compiledScript } -} - -object PebbleScriptFilter : Filter { - override fun getArgumentNames(): MutableList { - return mutableListOf("script") - } - override fun apply(input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any { - val scriptName = args["script"]?.toString() - ?: throw PebbleException(null, "Missing 'script' argument", lineNumber, self.name) - - val script = PebbleScriptLoader.loadFunction(scriptName) - ?: throw PebbleException(null, "Script $scriptName could not be found", lineNumber, self.name) - + fun runScript(scriptName: String, script: CompiledScript, input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { val bindings = SimpleBindings() bindings["text"] = input - bindings["ctx"] = java.util.function.Function(context::getVariable) + bindings["stdlib"] = PebbleScriptStdlib(bindings, self, lineNumber) + bindings["ctx"] = PebbleScriptVarContext(context::getVariable) bindings["args"] = args.toMutableMap().apply { remove("script") } return try { @@ -325,23 +316,66 @@ object PebbleScriptFilter : Filter { throw PebbleException(ex, "Unhandled ScriptException from $scriptName", lineNumber, self.name) } } -} - -object PebbleScriptFunction : Function { - override fun getArgumentNames(): MutableList { - return mutableListOf("script") + + fun runScript(scriptName: String, input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { + val script = loadFunction(scriptName) + ?: throw PebbleException(null, "Script $scriptName could not be found", lineNumber, self.name) + + return runScript(scriptName, script, input, args, self, context, lineNumber) } - override fun execute(args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any { + fun runScript(input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { val scriptName = args["script"]?.toString() ?: throw PebbleException(null, "Missing 'script' argument", lineNumber, self.name) + return runScript(scriptName, input, args, self, context, lineNumber) + } +} + +class PebbleScriptStdlib(private val bindings: Bindings, private val self: PebbleTemplate, private val lineNumber: Int) { + fun serialize(data: Any?): String { + return PebbleJsonLoader.deconvertJson(data).toString() + } + + fun deserialize(json: String): Any? { + return PebbleJsonLoader.convertJson(JsonStorageCodec.parseToJsonElement(json)) + } + + @JvmOverloads + fun loadScript(scriptName: String, env: Map = emptyMap()): Any? { val script = PebbleScriptLoader.loadFunction(scriptName) ?: throw PebbleException(null, "Script $scriptName could not be found", lineNumber, self.name) - val bindings = SimpleBindings() - bindings["ctx"] = java.util.function.Function(context::getVariable) - bindings["args"] = args.toMutableMap().apply { remove("script") } + val innerBindings = SimpleBindings() + innerBindings.putAll(env) + + return try { + script.eval(innerBindings) + } catch (ex: ScriptException) { + throw PebbleException(ex, "Unhandled ScriptException from $scriptName", lineNumber, self.name) + } + } + + fun loadScriptWith(scriptName: String, env: MutableMap = mutableMapOf()): Any? { + val script = PebbleScriptLoader.loadFunction(scriptName) + ?: throw PebbleException(null, "Script $scriptName could not be found", lineNumber, self.name) + + val innerBindings = SimpleBindings() + innerBindings.putAll(env) + + return try { + script.eval(innerBindings).also { _ -> + env.clear() + env.putAll(innerBindings) + } + } catch (ex: ScriptException) { + throw PebbleException(ex, "Unhandled ScriptException from $scriptName", lineNumber, self.name) + } + } + + fun loadScriptHere(scriptName: String): Any? { + val script = PebbleScriptLoader.loadFunction(scriptName) + ?: throw PebbleException(null, "Script $scriptName could not be found", lineNumber, self.name) return try { script.eval(bindings) @@ -350,3 +384,27 @@ object PebbleScriptFunction : Function { } } } + +fun interface PebbleScriptVarContext { + operator fun get(name: String): Any? +} + +object PebbleScriptFilter : Filter { + override fun getArgumentNames(): MutableList { + return mutableListOf("script") + } + + override fun apply(input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { + return PebbleScriptLoader.runScript(input, args, self, context, lineNumber) + } +} + +object PebbleScriptFunction : Function { + override fun getArgumentNames(): MutableList { + return mutableListOf("script") + } + + override fun execute(args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { + return PebbleScriptLoader.runScript(null, args, self, context, lineNumber) + } +} diff --git a/src/jvmMain/resources/static/init.js b/src/jvmMain/resources/static/init.js index 6dfb458..2efe1d9 100644 --- a/src/jvmMain/resources/static/init.js +++ b/src/jvmMain/resources/static/init.js @@ -260,6 +260,131 @@ } }); + window.renderVocab = function (vocab) { + function renderWord(word, index) { + const wordRoot = document.createElement("div"); + + const wordLabel = document.createElement("strong"); + wordLabel.append(word); + const indexLabel = document.createElement("i"); + indexLabel.append("definition " + (index + 1)); + wordRoot.appendChild(document.createElement("p")).append(wordLabel, "\u00A0", indexLabel); + + const definition = vocab.words[word][index]; + const inflection = vocab.inflections[definition.type]; + + const defList = wordRoot.appendChild(document.createElement("ol")); + for (const def of definition.definitions) { + defList.appendChild(document.createElement("li")).append(def); + } + + const inflectionTable = wordRoot.appendChild(document.createElement("table")); + for (const row of inflection) { + const rowElem = inflectionTable.appendChild(document.createElement("tr")); + for (const cell of row) { + const cellElem = rowElem.appendChild(document.createElement(cell.tag)); + for (const attr of Object.keys(cell.attrs)) { + cellElem.setAttribute(attr, cell.attrs[attr]); + } + if ((typeof cell.text) === "string") { + cellElem.innerHTML = cell.text; + } else { + cellElem.innerHTML = definition.forms[cell.text.form].replace(RegExp(cell.text.regexp, "ui"), cell.text.replacement); + } + } + } + + return wordRoot; + } + + const localeCompareSorter = (a, b) => a.localeCompare(b); + + const englishToWord = {}; + for (const word of Object.keys(vocab.words).sort(localeCompareSorter)) { + const definitions = vocab.words[word]; + const definitionsLength = definitions.length; + for (let i = 0; i < definitionsLength; i++) { + for (const keyword of definitions[i].inEnglish) { + const english = englishToWord[keyword] || (englishToWord[keyword] = []); + english.push({"word": word, "index": i}); + } + } + } + + const vocabRoot = document.createElement("div"); + const vocabSearchRoot = vocabRoot.appendChild(document.createElement("form")); + const vocabSearchResults = vocabRoot.appendChild(document.createElement("div")); + vocabSearchResults.appendChild(document.createElement("i")).append("Search results will appear here"); + + const vocabSearch = vocabSearchRoot.appendChild(document.createElement("input")); + vocabSearch.name = "q"; + vocabSearch.type = "text"; + + const vocabEnglishToLangRoot = vocabSearchRoot.appendChild(document.createElement("label")); + const vocabEnglishToLang = vocabEnglishToLangRoot.appendChild(document.createElement("input")); + vocabEnglishToLang.name = "target"; + vocabEnglishToLang.type = "radio"; + vocabEnglishToLang.value = "from-english"; + vocabEnglishToLang.checked = true; + vocabEnglishToLangRoot.append("English to " + vocab.langName); + + vocabSearchRoot.appendChild(document.createElement("br")); + + const vocabLangToEnglishRoot = vocabSearchRoot.appendChild(document.createElement("label")); + const vocabLangToEnglish = vocabLangToEnglishRoot.appendChild(document.createElement("input")); + vocabLangToEnglish.name = "target"; + vocabLangToEnglish.type = "radio"; + vocabLangToEnglish.value = "to-english"; + vocabLangToEnglishRoot.append(vocab.langName + " to English"); + + const vocabSearchButton = vocabSearchRoot.appendChild(document.createElement("input")); + vocabSearchButton.type = "submit"; + vocabSearchButton.value = "Search"; + + vocabSearchRoot.onsubmit = function (ev) { + ev.preventDefault(); + + const searchTerm = vocabSearch.value.trim(); + + while (vocabSearchResults.hasChildNodes()) { + vocabSearchResults.firstChild.remove(); + } + + const searchResults = []; + if (vocabEnglishToLang.checked) { + for (const englishWord of Object.keys(englishToWord).sort(localeCompareSorter)) { + if (!englishWord.startsWith(searchTerm)) continue; + + for (const vocabItem of englishToWord[englishWord]) { + searchResults.push(vocabItem); + } + } + } else { + for (const langWord of Object.keys(vocab.words).sort(localeCompareSorter)) { + if (!langWord.startsWith(searchTerm)) continue; + + const numDefs = vocab.words[langWord].length; + for (let i = 0; i < numDefs; i++) { + searchResults.push({"word": langWord, "index": i}); + } + } + } + + if (searchResults.length === 0) { + vocabSearchResults.appendChild(document.createElement("i")).append("No results found"); + return; + } + + searchResults.sort((a, b) => (a.word === b.word) ? (a.index - b.index) : localeCompareSorter(a.word, b.word)); + + for (const searchResult of searchResults) { + vocabSearchResults.append(renderWord(searchResult.word, searchResult.index)); + } + }; + + document.currentScript.after(vocabRoot); + }; + window.renderQuiz = function (quiz) { const quizFunctions = {}; const quizRoot = document.createElement("table");