///|
pub(all) struct DossierEvidence {
  id : String
  title : String
  source : String
  proof_path : String
  summary : String
  required : Bool
  complete : Bool
} derive(Eq, Debug)

///|
pub fn DossierEvidence::DossierEvidence(
  id? : String = "",
  title? : String = "",
  source? : String = "",
  proof_path? : String = "",
  summary? : String = "",
  required? : Bool = false,
  complete? : Bool = false,
) -> DossierEvidence {
  { id, title, source, proof_path, summary, required, complete }
}

///|
pub(all) struct DossierTimelineEvent {
  date : String
  actor : String
  action : String
  evidence_id : String
} derive(Eq, Debug)

///|
pub fn DossierTimelineEvent::DossierTimelineEvent(
  date? : String = "",
  actor? : String = "",
  action? : String = "",
  evidence_id? : String = "",
) -> DossierTimelineEvent {
  { date, actor, action, evidence_id }
}

///|
pub(all) struct DossierRisk {
  id : String
  title : String
  level : String
  mitigation : String
  resolved : Bool
} derive(Eq, Debug)

///|
pub fn DossierRisk::DossierRisk(
  id? : String = "",
  title? : String = "",
  level? : String = "medium",
  mitigation? : String = "",
  resolved? : Bool = false,
) -> DossierRisk {
  { id, title, level, mitigation, resolved }
}

///|
pub(all) struct DossierSection {
  title : String
  body : String
  evidence_ids : Array[String]
} derive(Eq, Debug)

///|
pub fn DossierSection::DossierSection(
  title? : String = "",
  body? : String = "",
  evidence_ids? : Array[String] = [],
) -> DossierSection {
  { title, body, evidence_ids }
}

///|
pub(all) struct DossierFinding {
  id : String
  severity : Severity
  title : String
  detail : String
  action : String
  evidence : String
  penalty : Int
} derive(Eq, Debug)

///|
fn dossier_finding(
  id : String,
  severity : Severity,
  title : String,
  detail : String,
  action : String,
  evidence : String,
  penalty : Int,
) -> DossierFinding {
  { id, severity, title, detail, action, evidence, penalty }
}

///|
pub(all) struct DossierProfile {
  applicant : String
  contact : String
  project_title : String
  package_name : String
  repository : String
  deadline : String
  review_notice : String
  relation_statement : String
  evidence : Array[DossierEvidence]
  timeline : Array[DossierTimelineEvent]
  risks : Array[DossierRisk]
} derive(Eq, Debug)

///|
pub fn DossierProfile::DossierProfile(
  applicant? : String = "",
  contact? : String = "",
  project_title? : String = "",
  package_name? : String = "",
  repository? : String = "",
  deadline? : String = "",
  review_notice? : String = "",
  relation_statement? : String = "",
  evidence? : Array[DossierEvidence] = [],
  timeline? : Array[DossierTimelineEvent] = [],
  risks? : Array[DossierRisk] = [],
) -> DossierProfile {
  {
    applicant,
    contact,
    project_title,
    package_name,
    repository,
    deadline,
    review_notice,
    relation_statement,
    evidence,
    timeline,
    risks,
  }
}

///|
pub(all) struct DossierReport {
  verdict : String
  score : Int
  ready : Bool
  evidence_count : Int
  complete_evidence_count : Int
  missing_required_count : Int
  unresolved_risk_count : Int
  timeline_count : Int
  action_count : Int
  sections : Array[DossierSection]
  actions : Array[String]
  findings : Array[DossierFinding]
} derive(Eq, Debug)

///|
pub fn resubmission_dossier(profile : DossierProfile) -> DossierReport {
  let findings = Array::new(capacity=12)
  push_dossier_identity_findings(findings, profile)
  push_dossier_relation_findings(findings, profile)
  push_dossier_evidence_findings(findings, profile)
  push_dossier_timeline_findings(findings, profile)
  push_dossier_risk_findings(findings, profile)
  let mut penalty = 0
  let mut fail_count = 0
  for finding in findings {
    penalty += finding.penalty
    if finding.severity == Fail {
      fail_count += 1
    }
  }
  let score = clamp_score(100 - penalty)
  let missing = missing_required_evidence_count(profile.evidence)
  let unresolved = unresolved_risk_count(profile.risks)
  let complete = complete_evidence_count(profile.evidence)
  let ready = fail_count == 0 && missing == 0 && unresolved == 0 && score >= 90
  let verdict = if ready { "dossier-ready" } else { "needs-dossier-work" }
  let actions = dossier_actions(profile, findings)
  let sections = dossier_sections(profile)
  {
    verdict,
    score,
    ready,
    evidence_count: profile.evidence.length(),
    complete_evidence_count: complete,
    missing_required_count: missing,
    unresolved_risk_count: unresolved,
    timeline_count: profile.timeline.length(),
    action_count: actions.length(),
    sections,
    actions,
    findings,
  }
}

///|
pub fn resubmission_dossier_markdown(profile : DossierProfile) -> String {
  resubmission_dossier(profile).to_markdown(profile)
}

///|
pub fn DossierReport::to_markdown(
  self : DossierReport,
  profile : DossierProfile,
) -> String {
  let out = StringBuilder()
  out.write_string("# ReviewProof Dossier\n\n")
  out.write_string("- Applicant: " + profile.applicant + "\n")
  out.write_string("- Contact: " + profile.contact + "\n")
  out.write_string("- Project: " + profile.project_title + "\n")
  out.write_string("- Package: " + profile.package_name + "\n")
  out.write_string("- Repository: " + profile.repository + "\n")
  out.write_string("- Deadline: " + profile.deadline + "\n")
  out.write_string("- Verdict: " + self.verdict + "\n")
  out.write_string("- Score: " + self.score.to_string() + "/100\n")
  out.write_string(
    "- Required evidence missing: " +
    self.missing_required_count.to_string() +
    "\n",
  )
  out.write_string(
    "- Unresolved risks: " + self.unresolved_risk_count.to_string() + "\n\n",
  )
  out.write_string("## Review Notice\n\n")
  out.write_string(blank_fallback(profile.review_notice, "No notice recorded."))
  out.write_string("\n\n")
  for section in self.sections {
    out.write_string("## " + section.title + "\n\n")
    out.write_string(section.body)
    out.write_string("\n\n")
    if section.evidence_ids.length() > 0 {
      out.write_string("Evidence: ")
      out.write_string(join_ids(section.evidence_ids))
      out.write_string("\n\n")
    }
  }
  out.write_string("## Evidence Matrix\n\n")
  for item in profile.evidence {
    let status = if item.complete { "complete" } else { "missing" }
    let required = if item.required { "required" } else { "optional" }
    out.write_string("- `" + item.id + "` " + status + " " + required)
    out.write_string(": " + item.title)
    if item.proof_path != "" {
      out.write_string(" -- " + item.proof_path)
    }
    if item.summary != "" {
      out.write_string(" Summary: " + item.summary)
    }
    out.write_string("\n")
  }
  out.write_string("\n## Timeline\n\n")
  for event in profile.timeline {
    out.write_string("- " + event.date + " " + event.actor + ": ")
    out.write_string(event.action)
    if event.evidence_id != "" {
      out.write_string(" [" + event.evidence_id + "]")
    }
    out.write_string("\n")
  }
  out.write_string("\n## Risk Register\n\n")
  for risk in profile.risks {
    let status = if risk.resolved { "resolved" } else { "open" }
    out.write_string("- `" + risk.id + "` " + risk.level + " " + status)
    out.write_string(": " + risk.title)
    if risk.mitigation != "" {
      out.write_string(" Mitigation: " + risk.mitigation)
    }
    out.write_string("\n")
  }
  out.write_string("\n## Findings\n\n")
  for finding in self.findings {
    out.write_string("- [" + severity_text(finding.severity) + "] ")
    out.write_string(finding.id + ": " + finding.title)
    if finding.evidence != "" {
      out.write_string(" -- " + finding.evidence)
    }
    if finding.action != "" && finding.severity != Pass {
      out.write_string(" Action: " + finding.action)
    }
    out.write_string("\n")
  }
  if self.actions.length() > 0 {
    out.write_string("\n## Required Actions\n\n")
    for action in self.actions {
      out.write_string("- " + action + "\n")
    }
  }
  out.to_string()
}

///|
pub fn dossier_profile_from_review(feedback : ReviewFeedback) -> DossierProfile {
  let evidence = Array::new(capacity=feedback.extension_evidence.length() + 4)
  evidence.push(
    DossierEvidence(
      id="prior-work",
      title="Related prior-work disclosure",
      source="review notice and Mooncakes manifest",
      proof_path="README.md / SUBMISSION.md / docs/review-response.md",
      summary=prior_summary(feedback.prior_work),
      required=true,
      complete=feedback.prior_work.length() > 0,
    ),
  )
  evidence.push(
    DossierEvidence(
      id="relation",
      title="Relation statement",
      source="corrected proposal",
      proof_path="SUBMISSION.md",
      summary=feedback.relation_statement,
      required=true,
      complete=feedback.relation_statement != "",
    ),
  )
  evidence.push(
    DossierEvidence(
      id="review-report",
      title="Reviewer-facing correction brief",
      source="ReviewProof generated report",
      proof_path="docs/review-response.md",
      summary="Resubmission brief names the prior work and concrete extension scope.",
      required=true,
      complete=true,
    ),
  )
  for item in feedback.extension_evidence {
    evidence.push(evidence_from_extension(item))
  }
  let timeline = [
    DossierTimelineEvent(
      date="2026-08-20",
      actor="review committee",
      action="Initial rejection notice reported high overlap.",
      evidence_id="prior-work",
    ),
    DossierTimelineEvent(
      date="2026-08-20",
      actor="applicant",
      action="Project scope changed to review feedback correction and resubmission proof.",
      evidence_id="relation",
    ),
    DossierTimelineEvent(
      date="2026-08-20",
      actor="applicant",
      action="MoonBit implementation, tests and review response documents were added.",
      evidence_id="review-report",
    ),
  ]
  let risks = [
    DossierRisk(
      id="overlap",
      title="Old HarborCheck scope overlaps with related prior work",
      level="high",
      mitigation="Disclose EJJ-ai-nb/harborcheck and claim only the ReviewProof resubmission workflow as new scope.",
      resolved=feedback.prior_work.length() > 0 &&
        feedback.relation_statement != "",
    ),
    DossierRisk(
      id="renaming-only",
      title="Correction could look like a rename without implementation",
      level="high",
      mitigation="Ship reviewproof.mbt, new tests, review-response docs and smoke output.",
      resolved=implemented_extension_count(feedback.extension_evidence) >= 3,
    ),
    DossierRisk(
      id="material-gap",
      title="Proposal could omit the extension relationship again",
      level="medium",
      mitigation="Keep README, SUBMISSION, research notes and review response in sync.",
      resolved=feedback.relation_statement != "",
    ),
  ]
  DossierProfile(
    applicant="武步",
    contact="19733880451 / wubu1234@qq.com",
    project_title=feedback.project_title,
    package_name=feedback.applicant_package,
    repository=feedback.repository,
    deadline=feedback.resubmit_deadline,
    review_notice=feedback.notice,
    relation_statement=feedback.relation_statement,
    evidence~,
    timeline~,
    risks~,
  )
}

///|
pub fn example_dossier_profile() -> DossierProfile {
  dossier_profile_from_review(example_review_feedback())
}

///|
pub fn evidence_table_markdown(evidence : Array[DossierEvidence]) -> String {
  let out = StringBuilder()
  out.write_string("| id | status | required | proof | summary |\n")
  out.write_string("| --- | --- | --- | --- | --- |\n")
  for item in evidence {
    out.write_string("| `" + item.id + "` | ")
    out.write_string(if item.complete { "complete" } else { "missing" })
    out.write_string(" | ")
    out.write_string(if item.required { "yes" } else { "no" })
    out.write_string(" | ")
    out.write_string(table_cell(item.proof_path))
    out.write_string(" | ")
    out.write_string(table_cell(item.summary))
    out.write_string(" |\n")
  }
  out.to_string()
}

///|
pub fn risk_register_markdown(risks : Array[DossierRisk]) -> String {
  let out = StringBuilder()
  out.write_string("| id | level | status | mitigation |\n")
  out.write_string("| --- | --- | --- | --- |\n")
  for risk in risks {
    out.write_string("| `" + risk.id + "` | ")
    out.write_string(risk.level + " | ")
    out.write_string(if risk.resolved { "resolved" } else { "open" })
    out.write_string(" | ")
    out.write_string(table_cell(risk.mitigation))
    out.write_string(" |\n")
  }
  out.to_string()
}

///|
pub fn timeline_markdown(events : Array[DossierTimelineEvent]) -> String {
  let out = StringBuilder()
  out.write_string("| date | actor | action | evidence |\n")
  out.write_string("| --- | --- | --- | --- |\n")
  for event in events {
    out.write_string("| " + table_cell(event.date))
    out.write_string(" | " + table_cell(event.actor))
    out.write_string(" | " + table_cell(event.action))
    out.write_string(" | " + table_cell(event.evidence_id))
    out.write_string(" |\n")
  }
  out.to_string()
}

///|
pub fn dossier_summary_line(report : DossierReport) -> String {
  "dossier=" +
  report.verdict +
  " score=" +
  report.score.to_string() +
  " evidence=" +
  report.complete_evidence_count.to_string() +
  "/" +
  report.evidence_count.to_string() +
  " missing=" +
  report.missing_required_count.to_string() +
  " risks=" +
  report.unresolved_risk_count.to_string()
}

///|
fn push_dossier_identity_findings(
  findings : Array[DossierFinding],
  profile : DossierProfile,
) -> Unit {
  if profile.applicant == "" || profile.contact == "" {
    findings.push(
      dossier_finding(
        "dossier.applicant",
        Fail,
        "Applicant and contact are recorded",
        "The dossier must identify the applicant and contact channel.",
        "Fill applicant and contact fields.",
        profile.applicant + " " + profile.contact,
        15,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.applicant",
        Pass,
        "Applicant and contact are recorded",
        "",
        "",
        profile.applicant,
        0,
      ),
    )
  }
  if profile.package_name == "" || profile.repository == "" {
    findings.push(
      dossier_finding(
        "dossier.links",
        Fail,
        "Package and repository links are recorded",
        "The dossier must tell reviewers where to inspect the corrected work.",
        "Fill package_name and repository.",
        profile.package_name + " " + profile.repository,
        15,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.links",
        Pass,
        "Package and repository links are recorded",
        "",
        "",
        profile.package_name + " / " + profile.repository,
        0,
      ),
    )
  }
}

///|
fn push_dossier_relation_findings(
  findings : Array[DossierFinding],
  profile : DossierProfile,
) -> Unit {
  if profile.review_notice == "" {
    findings.push(
      dossier_finding(
        "dossier.notice",
        Warn,
        "Review notice is recorded",
        "The dossier should quote or summarize the rejection reason.",
        "Add the review notice summary.",
        "",
        5,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.notice",
        Pass,
        "Review notice is recorded",
        "",
        "",
        profile.review_notice,
        0,
      ),
    )
  }
  if profile.relation_statement == "" {
    findings.push(
      dossier_finding(
        "dossier.relation",
        Fail,
        "Relation and extension statement is recorded",
        "The dossier must explain how this project differs from related prior work.",
        "Add a relation statement.",
        "",
        20,
      ),
    )
  } else if !contains_any(profile.relation_statement, [
      "related", "prior", "EJJ-ai-nb/harborcheck", "扩展", "关系",
    ]) {
    findings.push(
      dossier_finding(
        "dossier.relation",
        Warn,
        "Relation and extension statement is explicit",
        "The relation statement exists but does not clearly name prior work or extension.",
        "Mention the prior package and the new extension boundary.",
        profile.relation_statement,
        8,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.relation",
        Pass,
        "Relation and extension statement is recorded",
        "",
        "",
        profile.relation_statement,
        0,
      ),
    )
  }
}

///|
fn push_dossier_evidence_findings(
  findings : Array[DossierFinding],
  profile : DossierProfile,
) -> Unit {
  let missing = missing_required_evidence_count(profile.evidence)
  let complete = complete_evidence_count(profile.evidence)
  if profile.evidence.length() == 0 {
    findings.push(
      dossier_finding(
        "dossier.evidence",
        Fail,
        "Evidence matrix is present",
        "A dossier without evidence cannot support resubmission.",
        "Add required DossierEvidence records.",
        "",
        25,
      ),
    )
  } else if missing > 0 {
    findings.push(
      dossier_finding(
        "dossier.evidence",
        Fail,
        "Required evidence is complete",
        "Some required evidence records are missing proof.",
        "Complete required evidence before resubmission.",
        missing.to_string() + " missing required records",
        18 + missing * 5,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.evidence",
        Pass,
        "Required evidence is complete",
        "",
        "",
        complete.to_string() + "/" + profile.evidence.length().to_string(),
        0,
      ),
    )
  }
  if complete < 5 {
    findings.push(
      dossier_finding(
        "dossier.evidence-depth",
        Warn,
        "Evidence matrix has enough depth",
        "A stronger resubmission should include at least five complete records.",
        "Add tests, docs, package and CI evidence records.",
        complete.to_string() + " complete records",
        5,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.evidence-depth",
        Pass,
        "Evidence matrix has enough depth",
        "",
        "",
        complete.to_string() + " complete records",
        0,
      ),
    )
  }
}

///|
fn push_dossier_timeline_findings(
  findings : Array[DossierFinding],
  profile : DossierProfile,
) -> Unit {
  if profile.timeline.length() < 3 {
    findings.push(
      dossier_finding(
        "dossier.timeline",
        Warn,
        "Correction timeline is traceable",
        "A timeline helps reviewers see the correction process.",
        "Record rejection, implementation and publication events.",
        profile.timeline.length().to_string() + " events",
        5,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.timeline",
        Pass,
        "Correction timeline is traceable",
        "",
        "",
        profile.timeline.length().to_string() + " events",
        0,
      ),
    )
  }
  let missing_links = timeline_missing_evidence_count(profile.timeline)
  if missing_links > 0 {
    findings.push(
      dossier_finding(
        "dossier.timeline-links",
        Warn,
        "Timeline events link to evidence",
        "Some timeline events do not reference evidence ids.",
        "Add evidence_id to timeline events.",
        missing_links.to_string() + " events",
        missing_links * 2,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.timeline-links",
        Pass,
        "Timeline events link to evidence",
        "",
        "",
        "all events linked",
        0,
      ),
    )
  }
}

///|
fn push_dossier_risk_findings(
  findings : Array[DossierFinding],
  profile : DossierProfile,
) -> Unit {
  let unresolved = unresolved_risk_count(profile.risks)
  let high = unresolved_high_risk_count(profile.risks)
  if profile.risks.length() == 0 {
    findings.push(
      dossier_finding(
        "dossier.risks",
        Warn,
        "Risk register is present",
        "A resubmission dossier should show how overlap risk was handled.",
        "Add risk records for overlap, renaming-only and material gaps.",
        "",
        6,
      ),
    )
  } else if high > 0 {
    findings.push(
      dossier_finding(
        "dossier.risks",
        Fail,
        "High risks are resolved",
        "Open high risks mean the resubmission may be rejected again.",
        "Resolve high-risk items before resubmission.",
        high.to_string() + " open high risks",
        20 + high * 5,
      ),
    )
  } else if unresolved > 0 {
    findings.push(
      dossier_finding(
        "dossier.risks",
        Warn,
        "All risks are resolved",
        "Some non-high risks remain open.",
        "Resolve or explain remaining risks.",
        unresolved.to_string() + " open risks",
        5,
      ),
    )
  } else {
    findings.push(
      dossier_finding(
        "dossier.risks",
        Pass,
        "All risks are resolved",
        "",
        "",
        profile.risks.length().to_string() + " resolved risks",
        0,
      ),
    )
  }
}

///|
fn dossier_sections(profile : DossierProfile) -> Array[DossierSection] {
  [
    DossierSection(
      title="Correction Position",
      body=correction_position(profile),
      evidence_ids=["prior-work", "relation"],
    ),
    DossierSection(
      title="Implemented Difference",
      body=implemented_difference(profile.evidence),
      evidence_ids=complete_evidence_ids(profile.evidence),
    ),
    DossierSection(title="Reviewer Action", body=reviewer_action(profile), evidence_ids=[
      "review-report",
    ]),
  ]
}

///|
fn correction_position(profile : DossierProfile) -> String {
  let out = StringBuilder()
  out.write_string(
    "The corrected submission treats the old overlapping checklist scope as related prior work. ",
  )
  if profile.relation_statement != "" {
    out.write_string(profile.relation_statement)
  } else {
    out.write_string("A relation statement still needs to be added.")
  }
  out.to_string()
}

///|
fn implemented_difference(evidence : Array[DossierEvidence]) -> String {
  let out = StringBuilder()
  out.write_string(
    "The implementation evidence matrix contains " +
    complete_evidence_count(evidence).to_string() +
    " complete records. ",
  )
  out.write_string(
    "The strongest records should point to MoonBit code, tests, docs and package metadata.",
  )
  out.to_string()
}

///|
fn reviewer_action(profile : DossierProfile) -> String {
  "Review the corrected package " +
  profile.package_name +
  " in repository " +
  profile.repository +
  " and compare the documented extension boundary against the disclosed prior work."
}

///|
fn dossier_actions(
  profile : DossierProfile,
  findings : Array[DossierFinding],
) -> Array[String] {
  let actions = Array::new(capacity=8)
  for finding in findings {
    if finding.severity != Pass && finding.action != "" {
      actions.push(finding.action)
    }
  }
  for item in profile.evidence {
    if item.required && !item.complete {
      actions.push("Complete required evidence `" + item.id + "`.")
    }
  }
  for risk in profile.risks {
    if !risk.resolved {
      actions.push("Resolve risk `" + risk.id + "`: " + risk.title + ".")
    }
  }
  actions
}

///|
fn evidence_from_extension(item : ExtensionEvidence) -> DossierEvidence {
  DossierEvidence(
    id=slug_from_title(item.feature),
    title=item.feature,
    source="extension evidence",
    proof_path=item.proof,
    summary=item.value,
    required=true,
    complete=item.implemented && item.implementation != "" && item.proof != "",
  )
}

///|
fn complete_evidence_ids(evidence : Array[DossierEvidence]) -> Array[String] {
  let ids = Array::new(capacity=evidence.length())
  for item in evidence {
    if item.complete {
      ids.push(item.id)
    }
  }
  ids
}

///|
fn complete_evidence_count(evidence : Array[DossierEvidence]) -> Int {
  let mut count = 0
  for item in evidence {
    if item.complete {
      count += 1
    }
  }
  count
}

///|
fn missing_required_evidence_count(evidence : Array[DossierEvidence]) -> Int {
  let mut count = 0
  for item in evidence {
    if item.required && !item.complete {
      count += 1
    }
  }
  count
}

///|
fn unresolved_risk_count(risks : Array[DossierRisk]) -> Int {
  let mut count = 0
  for risk in risks {
    if !risk.resolved {
      count += 1
    }
  }
  count
}

///|
fn unresolved_high_risk_count(risks : Array[DossierRisk]) -> Int {
  let mut count = 0
  for risk in risks {
    if !risk.resolved && lower(risk.level) == "high" {
      count += 1
    }
  }
  count
}

///|
fn timeline_missing_evidence_count(events : Array[DossierTimelineEvent]) -> Int {
  let mut count = 0
  for event in events {
    if event.evidence_id == "" {
      count += 1
    }
  }
  count
}

///|
fn prior_summary(prior_work : Array[PriorWork]) -> String {
  if prior_work.length() == 0 {
    return "No related prior work recorded."
  }
  let out = StringBuilder()
  for i in 0.. 0 {
      out.write_string("; ")
    }
    let prior = prior_work[i]
    out.write_string(prior.package_name)
    if prior.version != "" {
      out.write_string("@" + prior.version)
    }
    if prior.overlap != "" {
      out.write_string(" overlap=" + prior.overlap)
    }
  }
  out.to_string()
}

///|
fn join_ids(ids : Array[String]) -> String {
  let out = StringBuilder()
  for i in 0.. 0 {
      out.write_string(", ")
    }
    out.write_string("`" + ids[i] + "`")
  }
  out.to_string()
}

///|
fn blank_fallback(text : String, fallback : String) -> String {
  if text == "" {
    fallback
  } else {
    text
  }
}

///|
fn table_cell(text : String) -> String {
  text.replace_all(old="|", new="\\|").replace_all(old="\n", new="
") } ///| fn slug_from_title(title : String) -> String { let out = StringBuilder() let lower_title = lower(title) let mut previous_dash = false for ch in lower_title { let code = ch.to_int() let digit = code >= 48 && code <= 57 let alpha = code >= 97 && code <= 122 if digit || alpha { out.write_char(ch) previous_dash = false } else if !previous_dash { out.write_string("-") previous_dash = true } } trim_dash(out.to_string()) } ///| fn trim_dash(text : String) -> String { let mut start = 0 let mut end = text.length() while start < end && text[start:start + 1] == "-" { start += 1 } while end > start && text[end - 1:end] == "-" { end -= 1 } if start >= end { "evidence" } else { text[start:end].to_owned() } }