From 2db56738f8614fc7b40e33fcdbbe60a7d7dcaa91 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Thu, 7 Mar 2024 18:15:38 +0800 Subject: [PATCH] add VarText utils --- .editorconfig | 10 +- .../sukazyo/cono/morny/core/MornyCoeur.scala | 11 +- .../core/bot/command/MornyInformation.scala | 145 +++++++++++------ .../cono/morny/data/MornyInformation.scala | 16 +- .../cono/morny/util/CommonFormat.scala | 2 +- .../cono/morny/util/ConvertByteHex.scala | 11 +- .../cono/morny/util/GivenContext.scala | 3 +- .../cono/morny/util/StringEnsure.scala | 5 +- .../sukazyo/cono/morny/util/UseString.scala | 108 +++++++++++++ .../cono/morny/util/dataview/Table.scala | 3 +- .../cono/morny/util/schedule/Task.scala | 3 +- .../cono/morny/util/var_text/VTNode.scala | 9 ++ .../morny/util/var_text/VTNodeLiteral.scala | 23 +++ .../cono/morny/util/var_text/VTNodeVar.scala | 16 ++ .../cono/morny/util/var_text/Var.scala | 94 +++++++++++ .../cono/morny/util/var_text/VarText.scala | 151 ++++++++++++++++++ .../test/utils/var_text/VarTextTest.scala | 21 +++ 17 files changed, 556 insertions(+), 75 deletions(-) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/UseString.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNode.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeLiteral.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeVar.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/var_text/VarTextTest.scala diff --git a/.editorconfig b/.editorconfig index 424d5bf..4a0e9b8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on ij_formatter_tags_enabled = true ij_smart_tabs = false -ij_visual_guides = +ij_visual_guides = 95 ij_wrap_on_typing = false [*.conf] @@ -528,7 +528,7 @@ ij_scala_preserve_space_after_method_declaration_name = false ij_scala_reformat_on_compile = false ij_scala_replace_case_arrow_with_unicode_char = false ij_scala_replace_for_generator_arrow_with_unicode_char = false -ij_scala_replace_lambda_with_greek_letter = false +ij_scala_replace_lambda_with_greek_letter = true ij_scala_replace_map_arrow_with_unicode_char = false ij_scala_scalafmt_config_path = ij_scala_scalafmt_fallback_to_default_settings = false @@ -847,6 +847,7 @@ ij_typescript_union_types_wrap = on_every_item ij_typescript_use_chained_calls_group_indents = false ij_typescript_use_double_quotes = true ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto ij_typescript_use_path_mapping = always ij_typescript_use_public_modifier = false ij_typescript_use_semicolon_after_statement = true @@ -1026,6 +1027,7 @@ ij_javascript_union_types_wrap = on_every_item ij_javascript_use_chained_calls_group_indents = false ij_javascript_use_double_quotes = false ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto ij_javascript_use_path_mapping = always ij_javascript_use_public_modifier = false ij_javascript_use_semicolon_after_statement = false @@ -1319,7 +1321,7 @@ ij_kotlin_wrap_elvis_expressions = 1 ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false -[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.conf,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config,mcmod.info,meatball_from_mutton.json,pack.mcmeta}] +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.conf,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config,mcmod.info,meatball_from_mutton.json,pack.mcmeta}] ij_smart_tabs = true ij_json_array_wrapping = split_into_lines ij_json_keep_blank_lines_in_code = 0 @@ -1401,7 +1403,7 @@ ij_markdown_min_lines_between_paragraphs = 1 ij_markdown_wrap_text_if_long = true ij_markdown_wrap_text_inside_blockquotes = true -[{*.pb,*.textproto}] +[{*.pb,*.textproto,*.txtpb}] indent_size = 2 indent_style = space tab_width = 2 diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala index 53748a5..f1b0f06 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala @@ -13,6 +13,7 @@ import cc.sukazyo.cono.morny.util.schedule.Scheduler import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.time.WatchDog import cc.sukazyo.cono.morny.util.GivenContext +import cc.sukazyo.cono.morny.util.UseString.MString import cc.sukazyo.cono.morny.util.UseThrowable.toLogString import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe @@ -119,12 +120,12 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes val externalContext: GivenContext = GivenContext() import cc.sukazyo.cono.morny.util.dataview.Table.format as fmtTable logger `info` - s"""The following Modules have been added to current Morny: + m"""The following Modules have been added to current Morny: |${fmtTable( - ("Module ID" :: "Module Name" :: "Module Version" :: Nil)::Nil ::: - modules.map(f => f.id :: f.name :: f.version :: Nil) - ).replaceAll("\n", "\n|")} - |""".stripMargin + "Module ID" :: "Module Name" :: "Module Version" :: Nil, + modules.map(f => f.id :: f.name :: f.version :: Nil)* + )} + |""" ///>>> BLOCK START instance configure & startup stage 1 diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala index 977af0d..33a54aa 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.core.bot.command import cc.sukazyo.cono.morny.core.{MornyCoeur, MornySystem} import cc.sukazyo.cono.morny.core.bot.api.{ICommandAlias, ITelegramCommand} import cc.sukazyo.cono.morny.core.bot.api.messages.{ErrorMessage, MessagingContext} +import cc.sukazyo.cono.morny.core.Log.logger import cc.sukazyo.cono.morny.data.MornyInformation.* import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.reporter.MornyReport @@ -10,13 +11,14 @@ import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute +import cc.sukazyo.cono.morny.util.var_text +import cc.sukazyo.cono.morny.util.var_text.VarText import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} import com.pengrad.telegrambot.TelegramBot import java.lang.System -// todo: maybe move some utils method outside class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { private given TelegramBot = coeur.account @@ -74,11 +76,19 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { cxt.bind_chat.id, getAboutPic ).caption( - s"""Morny Cono - |来自安妮的侍从小鼠。 - |———————————————— - |$getMornyAboutLinksHTML""" - .stripMargin + // language=html + (VarText( + """Morny Cono + |来自安妮的侍从小鼠。 + |———————————————— + |{about_links}""".stripMargin + ).render( + "about_links" -> getMornyAboutLinksHTML + ) :: Nil) + .map( f => + logger `trace` f + f + ).head ).parseMode(ParseMode HTML).replyToMessageId(cxt.bind_message.messageId) .unsafeExecute @@ -127,16 +137,26 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { val versionGitHTML = if (MornySystem.GIT_COMMIT nonEmpty) s"git $getVersionGitTagHTML" else "" SendMessage( event.message.chat.id, - // language=html - s"""version: - |- Morny ${h(MornySystem.CODENAME toUpperCase)} - |- ${h(MornySystem.VERSION_BASE)}$versionDeltaHTML${if (MornySystem.GIT_COMMIT nonEmpty) "\n- " + versionGitHTML else ""} - |coeur md5_hash: - |- ${h(MornySystem.getJarMD5)} - |coding timestamp: - |- ${MornySystem.CODE_TIMESTAMP} - |- ${h(formatDate(MornySystem.CODE_TIMESTAMP, 0))} [UTC] - |""".stripMargin + VarText( + // language=html + """version: + |- Morny {version_codename} + |- {version_base}{version_delta_html}{version_git_suffix} + |coeur md5_hash: + |- {md5} + |coding timestamp: + |- {time_millis} + |- {time_utc} [UTC] + |""".stripMargin + ).render( + "version_codename" -> h(MornySystem.CODENAME.toUpperCase), + "version_base" -> h(MornySystem.VERSION_BASE), + "version_delta_html" -> versionDeltaHTML, + "version_git_suffix" -> (if (MornySystem.GIT_COMMIT nonEmpty) "\n- " + versionGitHTML else ""), + "md5" -> h(MornySystem.getJarMD5), + "time_millis" -> MornySystem.CODE_TIMESTAMP, + "time_utc" -> h(formatDate(MornySystem.CODE_TIMESTAMP, 0)), + ) ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) .unsafeExecute } @@ -145,27 +165,48 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { def sysprop (p: String): String = System.getProperty(p) SendMessage( event.message.chat.id, - /* language=html */ - s"""system: - |- ${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "")} - |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} - |java runtime: - |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} - |- ${h(sysprop("java.vm.version"))} - |vm memory: - |- ${Runtime.getRuntime.totalMemory/1024/1024} / ${Runtime.getRuntime.maxMemory/1024/1024} MB - |- ${Runtime.getRuntime.availableProcessors} cores - |coeur version: - |- $getVersionAllFullTagHTML - |- ${h(MornySystem.getJarMD5)} - |- ${h(formatDate(MornySystem.CODE_TIMESTAMP, 0))} [UTC] - |- [${MornySystem.CODE_TIMESTAMP}] - |continuous: - |- ${h(formatDuration(System.currentTimeMillis - coeur.coeurStartTimestamp))} - |- [${System.currentTimeMillis - coeur.coeurStartTimestamp}] - |- ${h(formatDate(coeur.coeurStartTimestamp, 0))} - |- [${coeur.coeurStartTimestamp}]""" - .stripMargin + VarText( + /* language=html */ + """system: + |- {hostname} + |- {os.name} {os.arch} {os.version} + |java runtime: + |- {java.vm.vendor}.{java.vm.name} + |- {java.vm.version} + |vm memory: + |- {memory_used_mb} / {memory_available_mb} MB + |- {cpu_cores} cores + |coeur version: + |- {version_full} + |- {coeur_md5} + |- {compile_time_utc} [UTC] + |- [{compile_time_millis}] + |continuous: + |- {running_duration} + |- [{running_duration_ms}] + |- {startup_time_utc} + |- [{startup_time_millis}]""" + .stripMargin + ).render( + "hostname" -> h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else ""), + "os.name" -> h(sysprop("os.name")), + "os.arch" -> h(sysprop("os.arch")), + "os.version" -> h(sysprop("os.version")), + "java.vm.vendor" -> h(sysprop("java.vm.vendor")), + "java.vm.name" -> h(sysprop("java.vm.name")), + "java.vm.version" -> h(sysprop("java.vm.version")), + "memory_used_mb" -> (Runtime.getRuntime.totalMemory/1024/1024), + "memory_available_mb" -> (Runtime.getRuntime.maxMemory/1024/1024), + "cpu_cores" -> Runtime.getRuntime.availableProcessors, + "version_full" -> getVersionAllFullTagHTML, + "coeur_md5" -> h(MornySystem.getJarMD5), + "compile_time_utc" -> h(formatDate(MornySystem.CODE_TIMESTAMP, 0)), + "compile_time_millis" -> MornySystem.CODE_TIMESTAMP, + "running_duration" -> h(formatDuration(System.currentTimeMillis - coeur.coeurStartTimestamp)), + "running_duration_ms" -> (System.currentTimeMillis - coeur.coeurStartTimestamp), + "startup_time_utc" -> h(formatDate(coeur.coeurStartTimestamp, 0)), + "startup_time_millis" -> coeur.coeurStartTimestamp + ) ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) .unsafeExecute } @@ -174,12 +215,18 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { // if !coeur.trusted.isTrusted(update.message.from.id) then return; SendMessage( update.message.chat.id, - // language=html - s"""Coeur Task Scheduler: - | - scheduled tasks: ${coeur.tasks.amount} - | - scheduler status: ${coeur.tasks.state} - | - current runner status: ${coeur.tasks.runnerState} - |""".stripMargin + VarText( + // language=html + """Coeur Task Scheduler: + | - scheduled tasks: {coeur.tasks.amount} + | - scheduler status: {coeur.tasks.state} + | - current runner status: {coeur.tasks.runnerState} + |""".stripMargin + ).render( + "coeur.tasks.amount" -> coeur.tasks.amount, + "coeur.tasks.state" -> coeur.tasks.state, + "coeur.tasks.runnerState" -> coeur.tasks.runnerState + ) ).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId) .unsafeExecute } @@ -188,10 +235,14 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { coeur.externalContext >> { (reporter: MornyReport) => SendMessage( update.message.chat.id, - // language=html - s"""Event Statistics : - |in today - |${reporter.EventStatistics.eventStatisticsHTML}""".stripMargin + VarText( + // language=html + """Event Statistics : + |in today + |{event_statistics}""".stripMargin + ).render( + "event_statistics" -> reporter.EventStatistics.eventStatisticsHTML + ) ).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId) .unsafeExecute } || { diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala index 2e14d95..9718003 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala @@ -1,6 +1,7 @@ package cc.sukazyo.cono.morny.data import cc.sukazyo.cono.morny.core.{MornyAbout, MornySystem} +import cc.sukazyo.cono.morny.util.var_text.VarText import java.net.InetAddress import java.rmi.UnknownHostException @@ -39,9 +40,16 @@ object MornyInformation { def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get def getMornyAboutLinksHTML: String = - s"""source code | backup - |反馈 / issue tracker - |使用说明书 / user guide & docs""" - .stripMargin + VarText( + // language=html + """source code | backup + |反馈 / issue tracker + |使用说明书 / user guide & docs""".stripMargin + ).render( + "MORNY_SOURCECODE_LINK" -> MornyAbout.MORNY_SOURCECODE_LINK, + "MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK" -> MornyAbout.MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK, + "MORNY_ISSUE_TRACKER_LINK" -> MornyAbout.MORNY_ISSUE_TRACKER_LINK, + "MORNY_USER_GUIDE_LINK" -> MornyAbout.MORNY_USER_GUIDE_LINK + ) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala index 90ad990..e9f0fe1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala @@ -65,7 +65,7 @@ object CommonFormat { /** human readable [[String]] that describes the millis duration. * - * {{{ + * @example {{{ * scala> formatDuration(10) * val res0: String = 10ms * diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala index 0d469d4..6bd2d03 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala @@ -9,7 +9,10 @@ package cc.sukazyo.cono.morny.util * for example, byte `0` is binary `0000 0000`, it will be converted to * `"00"`, and the byte `-1` is binary `1111 1111` which corresponding * `"ff"`. - * {{{ + * + * while converting byte array, the order is: the 1st element of the array + * will be put most forward, then the following added to the tail of hex string. + * @example {{{ * scala> 0.toByte.toHex * val res6: String = 00 * @@ -18,11 +21,7 @@ package cc.sukazyo.cono.morny.util * * scala> -1.toByte.toHex * val res7: String = ff - * }}} - * - * while converting byte array, the order is: the 1st element of the array - * will be put most forward, then the following added to the tail of hex string. - * {{{ + * * scala> Array[Byte](0, 1, 2, 3).toHex * val res5: String = 00010203 * }}} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/GivenContext.scala b/src/main/scala/cc/sukazyo/cono/morny/util/GivenContext.scala index 0d3fe54..8f29661 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/GivenContext.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/GivenContext.scala @@ -23,8 +23,7 @@ object GivenContext { /** A mutable collection that can store(provide) any typed value and read(use/consume) that value by type. * - * ## Simple Guide - * {{{ + * @example {{{ * val cxt = GivenContext() * class BaseClass {} * class MyImplementation extends BaseClass {} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/StringEnsure.scala b/src/main/scala/cc/sukazyo/cono/morny/util/StringEnsure.scala index a0bfb76..c0121e3 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/StringEnsure.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/StringEnsure.scala @@ -2,7 +2,6 @@ package cc.sukazyo.cono.morny.util object StringEnsure { - extension (str: String) { /** Ensure the string have a length not smaller that the given length. @@ -36,8 +35,8 @@ object StringEnsure { * Notice that this method have un-defined behavior when the length of the String is less than * the character that will be kept, so change the character length that will be kept in your need. * - * Examples: - * {{{ + * + * @example {{{ * scala> val someUserToken = "TOKEN_UV:V927c092FV$REFV[p':V println(MString( + * "line1", // line 1 + * "line2", // line 2 + * "", // line 3 + * null, // will be ignored + * "line4", // line 4 + * )) + * line1 + * line2 + * + * line4 + * }}} + * + * @since 2.0.0 + */ + def MString (lines: String*): String = + lines.filterNot(_ == null).mkString("\n") + + /** A simple string interpolator implementation that fixed [[scala.collection.immutable.StringOps.stripMargin]] + * will remove the interpolated string's `|` when using s"" or f"" at the + * same time. + * + * @see [[m]] + */ + implicit class MString (private val sc: StringContext) extends AnyVal: + /** A simple string interpolator implementation that fixed [[scala.collection.immutable.StringOps.stripMargin]] + * will remove the interpolated string's `|` when using s"" or f"" at + * the same time. + * + * It will process stripMargin for each raw texts, then do the s"" + * interpolation. So, the interpolated string's margin character (`|`) + * will be kept. + * + * This should be useful when inserting a ascii table or ascii art when + * using string interpolator. + * + * @example {{{ + * scala> val table = + * raw""" +--------+ + * | | head | + * | +--------+ + * | | body | + * | | next | + * | +--------+ + * |""".stripMargin + * + * val table: String = " +--------+ + * | head | + * +--------+ + * | body | + * | next | + * +--------+ + * " + * + * scala> println(table) + * +--------+ + * | head | + * +--------+ + * | body | + * | next | + * +--------+ + * + * + * scala> println( + * s"""Here is a table: + * |$table + * |""".stripMargin) + * Here is a table: + * +--------+ + * head | + * +--------+ + * body | + * next | + * +--------+ + * + * + * + * scala> println( + * m"""Here is a table: + * |$table + * |""") + * Here is a table: + * +--------+ + * | head | + * +--------+ + * | body | + * | next | + * +--------+ + * }}} + * + * @since 2.0.0 + */ + def m (args: Any*): String = { + StringContext(sc.parts.map(_.stripMargin)*) + .s(args*) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala b/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala index f4a31e9..126767c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala @@ -2,7 +2,8 @@ package cc.sukazyo.cono.morny.util.dataview object Table { - def format (table: Seq[Seq[Any]]): String = { + def format (title: Seq[Any], data: Seq[Any]*): String = { + val table = data.prepended(title) if (table.isEmpty) "" else { // Get column widths based on the maximum cell width in each column (+2 for a one character padding on each side) diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala index 6250d9a..7d36734 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala @@ -38,8 +38,7 @@ trait Task extends Ordered[Task] { /** Returns this task's object name and the task name. * - * for example: - * {{{ + * @example {{{ * scala> val task = new Task { * val name = "example-task" * val scheduledTimeMillis = 0 diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNode.scala b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNode.scala new file mode 100644 index 0000000..2d847f4 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNode.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.util.var_text + +trait VTNode { + + def render (vars: Map[String, String]): String + + def serialize: String + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeLiteral.scala b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeLiteral.scala new file mode 100644 index 0000000..265d911 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeLiteral.scala @@ -0,0 +1,23 @@ +package cc.sukazyo.cono.morny.util.var_text + +case class VTNodeLiteral ( + text: String +) extends VTNode { + + override def render (vars: Map[String, String]): String = + text + + override def toString: String = + val prefix = "literal|" + val prefix_following = " |" + val pt = text.split('\n') + (pt.headOption.map(prefix + _).getOrElse("") :: + pt.drop(1).map(prefix_following + _).toList) + .mkString("\n") + + override def serialize: String = + text + .replaceAll("/\\{", "//\\{") + .replaceAll("\\{", "/\\{") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeVar.scala b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeVar.scala new file mode 100644 index 0000000..7b5a4a3 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VTNodeVar.scala @@ -0,0 +1,16 @@ +package cc.sukazyo.cono.morny.util.var_text + +class VTNodeVar ( + var_id: String +) extends VTNode { + + override def render (vars: Map[String, String]): String = + vars.getOrElse(var_id, s"$${$var_id}") + + override def toString: String = + s"var_def(id={$var_id})" + + override def serialize: String = + s"{$var_id}" + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala new file mode 100644 index 0000000..9e88f89 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala @@ -0,0 +1,94 @@ +package cc.sukazyo.cono.morny.util.var_text + +import scala.language.implicitConversions + +/** A Var is a key-value pair, where the key is a string, and the value is also a string. + * + * This class is used to represent a variable in the [[VarText]]. + * + * You can just call the [[Var]] constructor to create a new Var, or use the implicit + * conversion to create a var from a tuple of ([[String]], [[String]]), or use the extension + * method [[Var.StringAsVarText.asVar]] to convert a [[String]] to a var. + * + * @since 2.0.0 + * + * @param id The key of the variable, also known as var-id. + * + * The var-id have some limitation on the characters that can be used in it. + * For details, see [[Var.isLegalId]]. An illegal id will cause the constructor + * throws [[IllegalArgumentException]]. + * + * @param text The text content of the variable. + */ +case class Var ( + id: String, + text: String +) { + + // todo: id limitation + id.foreach { c => + if (!Var.isLegalId(c)) + throw new IllegalArgumentException(s"Character $c (${c.toInt}) is not allowed in a var id") + } + + /** Create a new Var with the same text but different id. + * @since 2.0.0 + */ + def asId (id: String): Var = + Var(id, this.text) + + /** Create a new Var with the same id but different text. + * @since 2.0.0 + */ + Var(this.id, text) + + /** Unpack this Var into a ([[String]], [[String]]) tuple. + * @since 2.0.0 + */ + def unpackKV: (String, String) = + (id, text) + +} + +object Var { + + private val ID_AVAILABLE_SYMBOLS: Set[Char] = + "_-.*/\\|:#@%&?;,~" + .toCharArray.toSet + + /** Is this character is a legal var-id character. + * + * In other words, if this character is a letter, a digit, or one of the following + * symbols (`_` `-` `.` `*` `/` `\` `|` `:` `#` `@` `%` `&` `?` `;` `,` `~`), this + * character is allowed to shows in the [[Var.id]], we said this character is a + * legal var-id character. + * + * @since 2.0.0 + * + * @return `true` if this character is legal, false otherwise. + */ + def isLegalId (c: Char): Boolean = + c.isLetterOrDigit || ID_AVAILABLE_SYMBOLS.contains(c) + + /** Convert a tuple of ([[String]], [[String]]) to a Var. + * + * The first string of the tuple will be the [[Var.id]], and the second string + * will be the [[Var.text]] + * + * @since 2.0.0 + */ + implicit def StrStrTupleAsVar (tuple: (String, Any)): Var = + Var(tuple._1, tuple._2.toString) + + /** @see [[asVar]] */ + implicit class StringAsVarText (text: String): + /** Convert this string text to a [[Var]]. + * @since 2.0.0 + * @param id the var-id. + * @return a [[Var]] that the [[Var.text text]] is this string, and the [[Var.id id]] + * is the given id. + */ + def asVar (id: String): Var = + Var(id, text) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala new file mode 100644 index 0000000..c0ac344 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala @@ -0,0 +1,151 @@ +package cc.sukazyo.cono.morny.util.var_text + +import cc.sukazyo.cono.morny.util.var_text.Var.isLegalId + +/** A text/string template that may contains some named replaceable variables. It's concept may + * be similar with scala's `StringContext` or `GString` in groovy. + * + * A [[VarText]] can contains a stream of [[VTNode]]s, each nodes can be a [[VTNodeLiteral]] or + * a [[VTNodeVar]]. + * + * This can be rendered to a native [[String]] by calling [[render]] method with a set of [[Var]] + * variables. The [[VTNodeVar]] will look for the given [[Var]]s to find if there's a match, and + * replace itself with the value of the [[Var]], or output a placeholder if there's no match. + * + * @since 2.0.0 + */ +trait VarText { + + val nodes: List[VTNode] + + /** Render this VarText with the given `(var-key -> value)` map. + * @since 2.0.0 + */ + def render (vars: Map[String, String]): String = + nodes.map(_.render(vars)).mkString + + /** Render this VarText with the given [[Var]]s seq. + * @since 2.0.0 + */ + def render (vars: Var*): String = + render(Map.from(vars.toList.map(_.unpackKV))) + + /** Inspect the nodes of this VarText. + * + * Each node will be rendered to a line with the node types prefix. + * + * @since 2.0.0 + */ + override def toString: String = + nodes.map(_.toString).mkString("\n") + + /** Serialize this VarText to a template string. + * + * The return template string will be like the original template string that can be parsed + * by the [[VarText.apply(String)]] parser. + * + * @since 2.0.0 + */ + def serialize: String = + nodes.map(_.serialize).mkString + +} + +object VarText { + + def apply (_nodes: VTNode*): VarText = new VarText: + override val nodes: List[VTNode] = _nodes.toList + + private val symbol_escape = '/' + private val symbol_var_start = '{' + private val symbol_var_end = '}' + + /** Parse a serialized VarText template string to a [[VarText]] object. + * + * In the current standard, the `{}` will be parsed to a [[VTNodeVar]], unless it + * is escaped by the escape char `/`; And the escape char can and can only escape [[VTNodeVar]] + * starter `{` or escape char `/` itself, any other chars following the escape char will + * be treated both escape char itself and the following char as a normal char; And all others + * will be parsed to [[VTNodeLiteral]]. + * + * @since 2.0.0 + */ + def apply (template: String): VarText = { + + val _nodes = collection.mutable.ListBuffer[VTNode]() + + def newBuffer = StringBuilder() + var buffer: StringBuilder = newBuffer + def pushc (c: Char): Unit = + buffer += c + def buffer2literal(): Unit = + _nodes += VTNodeLiteral(buffer.toString) + buffer = newBuffer + def buffer2var(): Unit = + _nodes += VTNodeVar(buffer.toString drop 1) + buffer = newBuffer + sealed trait State + case class in_escape(it: Char) extends State + case object literal extends State + case object in_var_def extends State + var state: State = literal + + template.foreach { i => + + def push(): Unit = + buffer += i + + state match + case in_escape(e) => + i match + case f if f == symbol_var_start => + state = literal + push() + case f if f == symbol_escape => + state = literal + push() + case _ => + state = literal + pushc(e) + push() + case _: in_var_def.type => + i match + case f if f == symbol_var_end => + buffer2var() + state = literal + case _ if isLegalId(i) => + push() + case _ => + state = literal + push() + case _: literal.type => + i match + case f if f == symbol_escape => + state = in_escape(i) + case f if f == symbol_var_start => + buffer2literal() + state = in_var_def + push() + case _ => + push() + + } + + state match + case in_escape(e) => + pushc(e) + case _ => + buffer2literal() + + new VarText: + override val nodes: List[VTNode] = + _nodes.toList + .filterNot { + case VTNodeLiteral(text) if text.isEmpty => + true + case _ => false + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/var_text/VarTextTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/var_text/VarTextTest.scala new file mode 100644 index 0000000..569d75a --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/var_text/VarTextTest.scala @@ -0,0 +1,21 @@ +package cc.sukazyo.cono.morny.test.utils.var_text + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.var_text.{VarText, VTNodeLiteral, VTNodeVar} + +class VarTextTest extends MornyTests { + + "VarText template convertor works." in { + VarText("abcdefg {one_var}{following}it /{escaped}it and this is //double-escape-literal, with a /no-need-to-escape then {{non formatted}}xxx {missing_part") + .toString + .shouldEqual(VarText( + VTNodeLiteral("abcdefg "), + VTNodeVar("one_var"), + VTNodeVar("following"), + VTNodeLiteral("it {escaped}it and this is /double-escape-literal, with a /no-need-to-escape then "), + VTNodeLiteral("{{non formatted}}xxx "), + VTNodeLiteral("{missing_part"), + ).toString) + } + +}