///|
fn html_escape(text : String) -> String {
text
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
.replace_all(old="'", new="'")
}
///|
fn markdown_escape(text : String) -> String {
html_escape(text)
.replace_all(old="\\", new="\\\\")
.replace_all(old="|", new="\\|")
.replace_all(old="\r", new="")
.replace_all(old="\n", new="
")
}
///|
fn percent(value : Int?) -> String {
guard value is Some(value) else { return "n/a" }
let sign = if value < 0 { "-" } else { "+" }
let absolute = if value < 0 { -value.to_int64() } else { value.to_int64() }
let fraction = (absolute % 100).to_string().pad_start(2, '0')
"\{sign}\{absolute / 100}.\{fraction}%"
}
///|
fn mm_or_missing(value : Int?) -> String {
match value {
Some(value) => format_diameter_mm(value)
None => "n/a"
}
}
///|
/// Text report of target-only categories, references and individual findings.
pub fn render_audit_markdown(trajectory : @evidence.Trajectory) -> String {
render_audit_markdown_in(trajectory, English)
}
///|
/// Localized presentation; input-derived text and machine codes are preserved.
pub fn render_audit_markdown_in(
trajectory : @evidence.Trajectory,
language : ReportLanguage,
) -> String {
let out = StringBuilder()
out.write_string(
tr(
language, "# Target-lesion follow-up review\n\nStudy: ", "# 靶病灶随访复核\n\n研究编号: ",
) +
markdown_escape(trajectory.study.id) +
"\n\n",
)
out.write_string(
tr(
language, "RECIST 1.1 target subset. Categories describe target lesions only.\n\n",
"RECIST 1.1 靶病灶子集。分类仅描述靶病灶。\n\n",
),
)
out.write_string(
tr(
language, "| Visit | Date | Target category | Sum (mm) | From baseline | Prior nadir (mm) | Nadir visit | From nadir |\n|---|---|---|---:|---:|---:|---|---:|\n",
"| 随访 | 日期 | 靶病灶分类 | 总和 (mm) | 较基线 | 此前最低值 (mm) | 最低值来源随访 | 较最低值 |\n|---|---|---|---:|---:|---:|---|---:|\n",
),
)
for a in trajectory.assessments {
out.write_string(
"| " +
markdown_escape(a.visit_id) +
" | " +
a.date +
" | " +
a.response.code() +
" | " +
mm_or_missing(a.sum_tenths_mm) +
" | " +
percent(a.baseline_change_basis_points) +
" | " +
format_diameter_mm(a.nadir_sum_tenths_mm) +
" | " +
markdown_escape(a.nadir_visit_id) +
" | " +
percent(a.nadir_change_basis_points) +
" |\n",
)
}
for a in trajectory.assessments {
out.write_string(
"\n## " + markdown_escape(a.visit_id) + " / " + a.response.code() + "\n\n",
)
for m in a.measurements {
out.write_string(
"- " +
markdown_escape(m.lesion_id) +
": " +
observation_label(m.observation, language) +
tr(language, ", effective ", ",采用直径 ") +
mm_or_missing(m.effective_tenths_mm) +
" mm\n",
)
}
for f in a.findings {
out.write_string(
"- `" +
f.code +
"`: " +
markdown_escape(finding_label(f, language)) +
"\n",
)
}
out.write_string(trace_markdown(a, language))
}
out.write_string(
render_lesion_section_markdown(trajectory, language) +
"\n" +
glossary(language) +
"\n",
)
out.to_string()
}
///|
fn csv_cell(value : String) -> String {
if value.contains_char(',') ||
value.contains_char('"') ||
value.contains_char('\n') ||
value.contains_char('\r') {
"\"" + value.replace_all(old="\"", new="\"\"") + "\""
} else {
value
}
}
///|
fn csv_row(values : Array[String]) -> String {
values.map(csv_cell).join(",") + "\n"
}
///|
/// Typed detail rows. Blank numeric fields represent unavailable values.
pub fn render_audit_csv(trajectory : @evidence.Trajectory) -> String {
let out = StringBuilder()
out.write_string(
"row_type,visit_id,date,target_response,lesion_id,kind,state,diameter_mm,sum_mm,baseline_sum_mm,prior_nadir_mm,prior_nadir_visit_id,baseline_change_pct,nadir_change_pct,code,message\n",
)
for a in trajectory.assessments {
let base = [
"assessment",
a.visit_id,
a.date,
a.response.code(),
"",
"",
"",
"",
match a.sum_tenths_mm {
Some(v) => format_diameter_mm(v)
None => ""
},
format_diameter_mm(a.baseline_sum_tenths_mm),
format_diameter_mm(a.nadir_sum_tenths_mm),
a.nadir_visit_id,
if a.baseline_change_basis_points is Some(_) {
percent(a.baseline_change_basis_points)
} else {
""
},
if a.nadir_change_basis_points is Some(_) {
percent(a.nadir_change_basis_points)
} else {
""
},
"",
"",
]
out.write_string(csv_row(base))
for m in a.measurements {
let row = base.copy()
row[0] = "measurement"
row[4] = m.lesion_id
row[5] = kind_name(m.kind)
row[6] = state_name(m.observation)
row[7] = match m.effective_tenths_mm {
Some(value) => format_diameter_mm(value)
None => ""
}
row[15] = match m.observation {
NotEvaluable(reason) => reason
_ => ""
}
out.write_string(csv_row(row))
}
for f in a.findings {
let row = base.copy()
row[0] = "finding"
row[4] = f.lesion_id.unwrap_or("")
row[14] = f.code
row[15] = f.message
out.write_string(csv_row(row))
}
}
out.to_string()
}