Add conlang vocabulary tag
authorLanius Trolling <lanius@laniustrolling.dev>
Tue, 23 Jan 2024 17:41:14 +0000 (12:41 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Tue, 23 Jan 2024 17:41:14 +0000 (12:41 -0500)
src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt
src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt
src/jvmMain/resources/static/init.js

index a011fac07cfb4893ac5e4274c74067ced68ee088..b5a44594c26cd2056d8b59f562a5e085c1fcbcbb 100644 (file)
@@ -62,6 +62,7 @@ enum class TextParserFormattingTagPlainText(val type: TextParserTagType<Unit>) {
        // Conlangs
        LANG(plainTextFormattingTag),
        ALPHABET(embeddedFormattingTag),
+       VOCAB(embeddedFormattingTag),
        ;
        
        companion object {
index c77a3cf16d0466ef6333df86ba52d110bfaba78f..0ff8cff1abf6a86d0b29a0acdb2c01ec9503ec88 100644 (file)
@@ -378,6 +378,16 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                        } else ""
                }
        ),
+       VOCAB(
+               TextParserTagType.Indirect { _, content, _ ->
+                       if (content.isBlank())
+                               ""
+                       else {
+                               val contentJson = JsonStorageCodec.parseToJsonElement(TextParserState.uncensorText(content))
+                               "<script>window.renderVocab($contentJson);</script>"
+                       }
+               }
+       ),
        ;
        
        companion object {
index d20bdff4e76c64e9f4f4703e68ba2ddee7f4cdb1..1ca849013e69b17f20b10cc09a960235bdb8a849 100644 (file)
@@ -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<PebbleTemplateCacheKey> {
        
        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<String, Any>?, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any {
+       override fun apply(input: Any?, args: MutableMap<String, Any?>, 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<String, Any>?, self: PebbleTemplate?, context: EvaluationContext?, lineNumber: Int): Any? {
+       override fun apply(input: Any?, args: MutableMap<String, Any?>, 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<String, Any>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? {
+       override fun execute(args: MutableMap<String, Any?>, 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<String> {
-               return mutableListOf("script")
-       }
        
-       override fun apply(input: Any?, args: MutableMap<String, Any?>, 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<String, Any?>, 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<String> {
-               return mutableListOf("script")
+       
+       fun runScript(scriptName: String, input: Any?, args: MutableMap<String, Any?>, 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<String, Any>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any {
+       fun runScript(input: Any?, args: MutableMap<String, Any?>, 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<String, Any?> = 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<String, Any?> = 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<String> {
+               return mutableListOf("script")
+       }
+       
+       override fun apply(input: Any?, args: MutableMap<String, Any?>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? {
+               return PebbleScriptLoader.runScript(input, args, self, context, lineNumber)
+       }
+}
+
+object PebbleScriptFunction : Function {
+       override fun getArgumentNames(): MutableList<String> {
+               return mutableListOf("script")
+       }
+       
+       override fun execute(args: MutableMap<String, Any?>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? {
+               return PebbleScriptLoader.runScript(null, args, self, context, lineNumber)
+       }
+}
index 6dfb458d1ba091a1589d4bf7704e4b222332440c..2efe1d9f8132fc78c0791000263ff687b3c2f738 100644 (file)
                }
        });
 
+       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");