mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-22 03:04:54 +08:00
Compare commits
2 Commits
d180e7d04f
...
7e3588c221
Author | SHA1 | Date | |
---|---|---|---|
7e3588c221 | |||
2db56738f8 |
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
} || {
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ object CommonFormat {
|
||||
|
||||
/** human readable [[String]] that describes the millis duration.
|
||||
*
|
||||
* {{{
|
||||
* @example {{{
|
||||
* scala> formatDuration(10)
|
||||
* val res0: String = 10ms
|
||||
*
|
||||
|
@ -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
|
||||
* }}}
|
||||
|
@ -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 {}
|
||||
|
@ -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
|
||||
*
|
||||
|
108
src/main/scala/cc/sukazyo/cono/morny/util/UseString.scala
Normal file
108
src/main/scala/cc/sukazyo/cono/morny/util/UseString.scala
Normal 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*)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.util.var_text
|
||||
|
||||
trait VTNode {
|
||||
|
||||
def render (vars: Map[String, String]): String
|
||||
|
||||
def serialize: String
|
||||
|
||||
}
|
@ -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("\\{", "/\\{")
|
||||
|
||||
}
|
@ -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}"
|
||||
|
||||
}
|
94
src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala
Normal file
94
src/main/scala/cc/sukazyo/cono/morny/util/var_text/Var.scala
Normal 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)
|
||||
|
||||
}
|
151
src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala
Normal file
151
src/main/scala/cc/sukazyo/cono/morny/util/var_text/VarText.scala
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user