Compare commits

...

2 Commits

Author SHA1 Message Date
7e3588c221
update version tag 2024-03-07 18:27:35 +08:00
2db56738f8
add VarText utils 2024-03-07 18:15:38 +08:00
18 changed files with 558 additions and 77 deletions

View File

@ -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

View File

@ -8,9 +8,9 @@ object MornyConfiguration {
val MORNY_CODE_STORE = "https://github.com/Eyre-S/Coeur-Morny-Cono"
val MORNY_COMMIT_PATH = "https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s"
val VERSION = "2.0.0-alpha17"
val VERSION = "2.0.0-alpha18"
val VERSION_DELTA: Option[String] = None
val CODENAME = "guanggu"
val CODENAME = "xinzheng"
val SNAPSHOT = true

View File

@ -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

View File

@ -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"""<b>Morny Cono</b>
|来自安妮的侍从小鼠
|
|$getMornyAboutLinksHTML"""
.stripMargin
// language=html
(VarText(
"""<b>Morny Cono</b>
|来自安妮的侍从小鼠
|
|{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 <code>${h(MornySystem.CODENAME toUpperCase)}</code>
|- <code>${h(MornySystem.VERSION_BASE)}</code>$versionDeltaHTML${if (MornySystem.GIT_COMMIT nonEmpty) "\n- " + versionGitHTML else ""}
|coeur md5_hash:
|- <code>${h(MornySystem.getJarMD5)}</code>
|coding timestamp:
|- <code>${MornySystem.CODE_TIMESTAMP}</code>
|- <code>${h(formatDate(MornySystem.CODE_TIMESTAMP, 0))} [UTC]</code>
|""".stripMargin
VarText(
// language=html
"""version:
|- Morny <code>{version_codename}</code>
|- <code>{version_base}</code>{version_delta_html}{version_git_suffix}
|coeur md5_hash:
|- <code>{md5}</code>
|coding timestamp:
|- <code>{time_millis}</code>
|- <code>{time_utc} [UTC]</code>
|""".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:
|- <code>${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "<unknown-host>")}</code>
|- <code>${h(sysprop("os.name"))}</code> <code>${h(sysprop("os.arch"))}</code> <code>${h(sysprop("os.version"))}</code>
|java runtime:
|- <code>${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))}</code>
|- <code>${h(sysprop("java.vm.version"))}</code>
|vm memory:
|- <code>${Runtime.getRuntime.totalMemory/1024/1024}</code> / <code>${Runtime.getRuntime.maxMemory/1024/1024}</code> MB
|- <code>${Runtime.getRuntime.availableProcessors}</code> cores
|coeur version:
|- $getVersionAllFullTagHTML
|- <code>${h(MornySystem.getJarMD5)}</code>
|- <code>${h(formatDate(MornySystem.CODE_TIMESTAMP, 0))} [UTC]</code>
|- [<code>${MornySystem.CODE_TIMESTAMP}</code>]
|continuous:
|- <code>${h(formatDuration(System.currentTimeMillis - coeur.coeurStartTimestamp))}</code>
|- [<code>${System.currentTimeMillis - coeur.coeurStartTimestamp}</code>]
|- <code>${h(formatDate(coeur.coeurStartTimestamp, 0))}</code>
|- [<code>${coeur.coeurStartTimestamp}</code>]"""
.stripMargin
VarText(
/* language=html */
"""system:
|- <code>{hostname}</code>
|- <code>{os.name}</code> <code>{os.arch}</code> <code>{os.version}</code>
|java runtime:
|- <code>{java.vm.vendor}.{java.vm.name}</code>
|- <code>{java.vm.version}</code>
|vm memory:
|- <code>{memory_used_mb}</code> / <code>{memory_available_mb}</code> MB
|- <code>{cpu_cores}</code> cores
|coeur version:
|- {version_full}
|- <code>{coeur_md5}</code>
|- <code>{compile_time_utc} [UTC]</code>
|- [<code>{compile_time_millis}</code>]
|continuous:
|- <code>{running_duration}</code>
|- [<code>{running_duration_ms}</code>]
|- <code>{startup_time_utc}</code>
|- [<code>{startup_time_millis}</code>]"""
.stripMargin
).render(
"hostname" -> h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "<unknown-host>"),
"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"""<b>Coeur Task Scheduler:</b>
| - <i>scheduled tasks</i>: <code>${coeur.tasks.amount}</code>
| - <i>scheduler status</i>: <code>${coeur.tasks.state}</code>
| - <i>current runner status</i>: <code>${coeur.tasks.runnerState}</code>
|""".stripMargin
VarText(
// language=html
"""<b>Coeur Task Scheduler:</b>
| - <i>scheduled tasks</i>: <code>{coeur.tasks.amount}</code>
| - <i>scheduler status</i>: <code>{coeur.tasks.state}</code>
| - <i>current runner status</i>: <code>{coeur.tasks.runnerState}</code>
|""".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"""<b>Event Statistics :</b>
|in today
|${reporter.EventStatistics.eventStatisticsHTML}""".stripMargin
VarText(
// language=html
"""<b>Event Statistics :</b>
|in today
|{event_statistics}""".stripMargin
).render(
"event_statistics" -> reporter.EventStatistics.eventStatisticsHTML
)
).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId)
.unsafeExecute
} || {

View File

@ -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"""<a href='${MornyAbout MORNY_SOURCECODE_LINK}'>source code</a> | <a href='${MornyAbout MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK}'>backup</a>
|<a href='${MornyAbout MORNY_ISSUE_TRACKER_LINK}'>反馈 / issue tracker</a>
|<a href='${MornyAbout MORNY_USER_GUIDE_LINK}'>使用说明书 / user guide & docs</a>"""
.stripMargin
VarText(
// language=html
"""<a href='{MORNY_SOURCECODE_LINK}'>source code</a> | <a href='{MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK}'>backup</a>
|<a href='{MORNY_ISSUE_TRACKER_LINK}'>反馈 / issue tracker</a>
|<a href='{MORNY_USER_GUIDE_LINK}'>使用说明书 / user guide & docs</a>""".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
)
}

View File

@ -65,7 +65,7 @@ object CommonFormat {
/** human readable [[String]] that describes the millis duration.
*
* {{{
* @example {{{
* scala> formatDuration(10)
* val res0: String = 10ms
*

View File

@ -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
* }}}

View File

@ -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 {}

View File

@ -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<IE#*&@()U8eR)c"
* val someUserToken: String = TOKEN_UV:V927c092FV$REFV[p':V<IE#*&@()U8eR)c
*

View File

@ -0,0 +1,108 @@
package cc.sukazyo.cono.morny.util
object UseString {
/** Convert a list of [[String]] to a multiline string.
*
* Each input string will be a line in the new string, and the the string
* that equals to `null` will be ignored.
*
* @example {{{
* scala> 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*)
}
}

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,9 @@
package cc.sukazyo.cono.morny.util.var_text
trait VTNode {
def render (vars: Map[String, String]): String
def serialize: String
}

View File

@ -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("\\{", "/\\{")
}

View File

@ -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}"
}

View File

@ -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)
}

View File

@ -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 `{<param>}` 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
}
}
}

View File

@ -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)
}
}