///|
/// yisheng command line: check / plan / rhyme / identify / list.
const VERSION : String = "0.2.3"
///|
fn usage() -> Unit {
println("倚声 yisheng " + VERSION + " — 诗律词律引擎")
println("")
println("用法:")
println(
" yisheng check [--ci 词牌 | --form 五绝|七绝|五律|七律] [--json]",
)
println(" yisheng check --text \"诗句\" [--ci 词牌]")
println(" yisheng plan <诗体> [--variant 0-3]")
println(" yisheng plan --ci <词牌>")
println(
" yisheng rhyme <韵部|字> [--system pingshui|cilin] [--tone 平|仄]",
)
println(" yisheng identify ")
println(" yisheng list [词牌名关键词]")
println(" yisheng version")
println("")
println("示例:")
println(" yisheng check examples/denggao.txt")
println(" yisheng check examples/rumengling.txt --ci 如梦令 --json")
println(" yisheng plan 七律 --variant 2")
println(" yisheng rhyme 上平十灰 --tone 平")
}
///|
fn find_flag(argv : Array[String], name : String) -> String? {
let mut i = 0
while i + 1 < argv.length() {
if argv[i] == name {
return Some(argv[i + 1])
}
i = i + 1
}
None
}
///|
fn has_flag(argv : Array[String], name : String) -> Bool {
for a in argv {
if a == name {
return true
}
}
false
}
///|
fn is_command(a : String) -> Bool {
a == "check" ||
a == "plan" ||
a == "rhyme" ||
a == "identify" ||
a == "list" ||
a == "help" ||
a == "version"
}
///|
fn positionals(argv : Array[String]) -> Array[String] {
let out : Array[String] = []
let mut i = 0
// argv[0] is the executable; skip up to and including the subcommand.
while i < argv.length() {
if is_command(argv[i]) {
i = i + 1
break
}
i = i + 1
}
while i < argv.length() {
let a = argv[i]
if a == "--ci" ||
a == "--form" ||
a == "--text" ||
a == "--tone" ||
a == "--system" ||
a == "--variant" ||
a == "--file" {
i = i + 2
continue
}
if a == "--json" {
i = i + 1
continue
}
if a.length() > 0 && a.to_array()[0] != '-' {
out.push(a)
}
i = i + 1
}
out
}
///|
fn read_path(path : String) -> Result[String, String] {
let text = @fs.read_file_to_string(path) catch {
_ => return Err("无法读取文件:" + path)
}
Ok(text)
}
///|
fn read_input(argv : Array[String]) -> Result[String, String] {
match find_flag(argv, "--text") {
Some(t) => Ok(t)
None =>
match find_flag(argv, "--file") {
Some(path) => read_path(path)
None => {
let pos = positionals(argv)
if pos.length() >= 1 {
read_path(pos[0])
} else {
Err("缺少输入文件,或使用 --text 直接传入文本")
}
}
}
}
}
///|
fn tone_char(code : Char) -> Char {
match code {
'p' => '平'
'z' => '仄'
_ => '?'
}
}
///|
fn render_view(view : @yisheng.LineView) -> String {
let sb = StringBuilder()
let chars = view.text.to_array()
let actual = view.actual.to_array()
for i in 0.. String {
let sb = StringBuilder()
for c in view.pattern {
sb.write_char(
match c {
'p' => '平'
'z' => '仄'
_ => '中'
},
)
}
sb.to_string()
}
///|
fn issue_line(issue : @yisheng.Issue) -> String {
let loc = if issue.line == 0 {
""
} else if issue.col == 0 {
"第" + issue.line.to_string() + "句 "
} else {
"第" + issue.line.to_string() + "句第" + issue.col.to_string() + "字 "
}
"[" + issue.code + "] " + loc + issue.message
}
///|
fn render_human(report : @yisheng.Report) -> Unit {
println(report.form + " · " + report.template)
for note in report.notes {
println(" " + note)
}
println("")
for i in 0.. errors = errors + 1
@yisheng.Warning => warnings = warnings + 1
_ => ()
}
println(" " + issue_line(issue))
}
println("")
if report.ok {
println(
"结论:合律(错误 0,提示 " + warnings.to_string() + ")",
)
} else {
println(
"结论:不合律(错误 " +
errors.to_string() +
",提示 " +
warnings.to_string() +
")",
)
}
}
///|
fn json_escape(s : String) -> String {
let sb = StringBuilder()
for c in s {
match c {
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
'\n' => sb.write_string("\\n")
'\r' => sb.write_string("\\r")
'\t' => sb.write_string("\\t")
_ => sb.write_char(c)
}
}
sb.to_string()
}
///|
fn json_string(s : String) -> String {
"\"" + json_escape(s) + "\""
}
///|
fn render_json(report : @yisheng.Report) -> Unit {
let sb = StringBuilder()
sb.write_string("{")
sb.write_string("\"ok\":" + (if report.ok { "true" } else { "false" }))
sb.write_string(",\"form\":" + json_string(report.form))
sb.write_string(",\"template\":" + json_string(report.template))
sb.write_string(",\"rhyme\":" + json_string(report.rhyme))
sb.write_string(",\"notes\":[")
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string(json_string(report.notes[i]))
}
sb.write_string("],\"issues\":[")
for i in 0.. 0 {
sb.write_string(",")
}
let it = report.issues[i]
sb.write_string("{")
sb.write_string("\"code\":" + json_string(it.code))
sb.write_string(
",\"severity\":" + json_string(@yisheng.severity_label(it.severity)),
)
sb.write_string(",\"line\":" + it.line.to_string())
sb.write_string(",\"col\":" + it.col.to_string())
sb.write_string(",\"message\":" + json_string(it.message))
sb.write_string(",\"expected\":" + json_string(it.expected))
sb.write_string(",\"actual\":" + json_string(it.actual))
sb.write_string("}")
}
sb.write_string("],\"lines\":[")
for i in 0.. 0 {
sb.write_string(",")
}
let v = report.lines[i]
sb.write_string("{")
sb.write_string("\"text\":" + json_string(v.text))
sb.write_string(",\"pattern\":" + json_string(v.pattern))
sb.write_string(",\"actual\":" + json_string(v.actual))
sb.write_string("}")
}
sb.write_string("]}")
println(sb.to_string())
}
///|
fn looks_like_shi(text : String) -> Bool {
let lines = @yisheng.poem_lines(text)
if lines.length() != 4 && lines.length() != 8 {
return false
}
let w = lines[0].to_array().length()
if w != 5 && w != 7 {
return false
}
for l in lines {
if l.to_array().length() != w {
return false
}
}
true
}
///|
fn parse_shi_form(name : String) -> @yisheng.ShiForm? {
match name {
"五绝" => Some(@yisheng.WuJue)
"七绝" => Some(@yisheng.QiJue)
"五律" => Some(@yisheng.WuLv)
"七律" => Some(@yisheng.QiLv)
_ => None
}
}
///|
fn han_count(text : String) -> Int32 {
let mut n = 0
for line in @yisheng.poem_lines(text) {
n = n + line.length()
}
n
}
///|
fn cmd_check(lex : @yisheng.Lexicon, argv : Array[String]) -> Unit {
match read_input(argv) {
Err(msg) => {
println(msg)
@sys.exit(2)
}
Ok(text) => {
if han_count(text) == 0 {
println(
"输入中没有可判定的汉字,请检查文件内容或 --text 参数",
)
@sys.exit(2)
}
let report = match find_flag(argv, "--ci") {
Some(name) => @yisheng.check_ci(lex, text, Some(name))
None =>
match find_flag(argv, "--form") {
Some(form_name) =>
match parse_shi_form(form_name) {
Some(form) => @yisheng.check_shi(lex, text, Some(form))
None => {
println("未知诗体:" + form_name)
@sys.exit(2)
@yisheng.empty_report()
}
}
None =>
if looks_like_shi(text) {
@yisheng.check_shi(lex, text, None)
} else {
@yisheng.check_ci(lex, text, None)
}
}
}
if has_flag(argv, "--json") {
render_json(report)
} else {
render_human(report)
}
if !report.ok {
@sys.exit(1)
}
}
}
}
///|
fn cmd_plan(lex : @yisheng.Lexicon, argv : Array[String]) -> Unit {
match find_flag(argv, "--ci") {
Some(name) => {
let plans = @yisheng.plan_ci(lex, name)
if plans.length() == 0 {
println("内置词谱中没有该词牌:" + name)
@sys.exit(2)
}
for p in plans {
println(p.title)
for line in p.lines {
println(" " + line)
}
println("")
}
}
None => {
let pos = positionals(argv)
if pos.length() < 1 {
println("可用诗体:")
for f in @yisheng.form_names() {
println(" " + f)
}
return
}
match parse_shi_form(pos[0]) {
None => {
println("未知诗体:" + pos[0])
@sys.exit(2)
}
Some(form) => {
let variant = match find_flag(argv, "--variant") {
Some(v) =>
match @yisheng.parse_int_opt(v) {
Some(n) =>
if n < 0 || n > 3 {
println("--variant 只接受 0-3,收到:" + v)
@sys.exit(2)
0
} else {
n
}
None => {
println("--variant 需要 0-3 的整数,收到:" + v)
@sys.exit(2)
0
}
}
None => 0
}
let plan = @yisheng.plan_shi(form, variant)
println(plan.title)
for line in plan.lines {
println(" " + line)
}
println("")
println(
"说明:平/仄为定格,韵为韵脚;一三五不论见 README。",
)
}
}
}
}
}
///|
fn find_groups(
lex : @yisheng.Lexicon,
system : @yisheng.RhymeSystem,
needle : String,
) -> Array[String] {
let out : Array[String] = []
for g in @yisheng.group_names(lex, system) {
if g == needle {
return [g]
}
}
for g in @yisheng.group_names(lex, system) {
if g.contains(needle) {
out.push(g)
}
}
out
}
///|
fn cmd_rhyme(lex : @yisheng.Lexicon, argv : Array[String]) -> Unit {
let pos = positionals(argv)
if pos.length() < 1 {
println(
"用法:yisheng rhyme <韵部|字> [--system pingshui|cilin] [--tone 平|仄]",
)
@sys.exit(2)
}
let needle = pos[0]
let system = match find_flag(argv, "--system") {
Some("cilin") => @yisheng.Cilin
Some("pingshui") => @yisheng.Pingshui
Some(other) => {
println("未知韵书:" + other)
@sys.exit(2)
@yisheng.Pingshui
}
None => @yisheng.Pingshui
}
let want_ping = match find_flag(argv, "--tone") {
Some("平") => Some(true)
Some("仄") => Some(false)
Some(other) => {
println("未知声调:" + other)
@sys.exit(2)
None
}
None => None
}
let groups = find_groups(lex, system, needle)
if groups.length() > 0 {
for g in groups {
let chars = @yisheng.group_chars(lex, system, g)
let filtered : Array[Char] = []
for ch in chars {
let mut keep = want_ping == None
for r in @yisheng.lookup(lex, system, ch) {
match want_ping {
Some(p) =>
if @yisheng.is_ping(r.tone) == p && r.group == g {
keep = true
}
None => ()
}
}
if keep {
filtered.push(ch)
}
}
println(g + "(" + filtered.length().to_string() + " 字)")
let sb = StringBuilder()
for i in 0.. 0 && i % 24 == 0 {
println(sb.to_string())
let _ = sb.reset()
}
sb.write_char(filtered[i])
}
if filtered.length() > 0 {
println(sb.to_string())
}
println("")
}
return
}
let chars = needle.to_array()
if chars.length() == 1 {
let ch = chars[0]
let readings = @yisheng.lookup(lex, system, ch)
if readings.length() == 0 {
println("「" + needle + "」不在所选韵书中。")
@sys.exit(1)
}
println("「" + needle + "」的读音:")
for r in readings {
println(
" " +
@yisheng.tone_label(r.tone) +
" · " +
r.group +
" · " +
r.tone_class +
"声",
)
}
} else {
println("未找到韵部或单字:" + needle)
@sys.exit(1)
}
}
///|
fn cmd_identify(lex : @yisheng.Lexicon, argv : Array[String]) -> Unit {
match read_input(argv) {
Err(msg) => {
println(msg)
@sys.exit(2)
}
Ok(text) => {
if han_count(text) == 0 {
println(
"输入中没有可判定的汉字,请检查文件内容或 --text 参数",
)
@sys.exit(2)
}
if looks_like_shi(text) {
let r = @yisheng.check_shi(lex, text, None)
println("接近诗体:" + r.form + " · " + r.template)
println("问题数:" + r.issues.length().to_string())
for i in r.issues {
println(" " + issue_line(i))
}
println("")
}
let ranked = @yisheng.identify_ci(lex, text)
println("词牌近似度(差异越小越接近):")
for item in ranked {
let (name, score, sketch) = item
println(" " + name + " " + sketch + " 差异 " + score.to_string())
}
}
}
}
///|
fn print_wrapped(items : Array[String], per_line : Int32) -> Unit {
let sb = StringBuilder()
let mut count = 0
for i in 0.. 0 {
sb.write_string("、")
}
sb.write_string(items[i])
count = count + 1
if count == per_line {
println(" " + sb.to_string())
let _ = sb.reset()
count = 0
}
}
if count > 0 {
println(" " + sb.to_string())
}
}
///|
fn cmd_list(lex : @yisheng.Lexicon, argv : Array[String]) -> Unit {
let keyword = match find_flag(argv, "--ci") {
Some(k) => k
None => {
let pos = positionals(argv)
if pos.length() > 0 {
pos[0]
} else {
""
}
}
}
println("诗体:五绝、七绝、五律、七律")
println("")
let all_names = @yisheng.builtin_tune_names(lex)
let names : Array[String] = []
for n in all_names {
if keyword == "" || n.contains(keyword) {
names.push(n)
}
}
if keyword == "" {
println("内置词牌(" + all_names.length().to_string() + "):")
} else {
println(
"匹配「" +
keyword +
"」的词牌(" +
names.length().to_string() +
"/" +
all_names.length().to_string() +
"):",
)
}
print_wrapped(names, 8)
println("")
let groups = @yisheng.group_names(lex, @yisheng.Pingshui)
println("平水韵部(" + groups.length().to_string() + "):")
print_wrapped(groups, 8)
}
///|
fn main {
let argv = @env.args()
let mut cmd = ""
for a in argv {
if a == "check" ||
a == "plan" ||
a == "rhyme" ||
a == "identify" ||
a == "list" ||
a == "help" ||
a == "version" {
cmd = a
break
}
}
if has_flag(argv, "--version") || cmd == "version" {
println("yisheng " + VERSION)
return
}
if cmd == "help" || cmd == "" {
usage()
return
}
let lex = @yisheng.Lexicon::builtin()
if cmd == "check" {
cmd_check(lex, argv)
} else if cmd == "plan" {
cmd_plan(lex, argv)
} else if cmd == "rhyme" {
cmd_rhyme(lex, argv)
} else if cmd == "identify" {
cmd_identify(lex, argv)
} else if cmd == "list" {
cmd_list(lex, argv)
}
}