1use super::self_test::{CaseOutcome, OperationResult, SelfTestReport};
22use std::collections::BTreeMap;
23
24pub fn render_html(report: &SelfTestReport, audit: Option<&serde_json::Value>) -> String {
31 render_html_with_options(report, audit, &RenderOptions::default())
32}
33
34#[derive(Debug, Clone)]
40pub struct RenderOptions {
41 pub missed_cap: Option<usize>,
42}
43
44impl Default for RenderOptions {
45 fn default() -> Self {
46 Self {
47 missed_cap: Some(200),
48 }
49 }
50}
51
52pub fn render_html_with_options(
55 report: &SelfTestReport,
56 audit: Option<&serde_json::Value>,
57 opts: &RenderOptions,
58) -> String {
59 let mut html = String::new();
60 html.push_str(HEAD);
61 push_header(&mut html, report);
62 push_summary_cards(&mut html, report);
63 let anchors = compute_anchor_set(report, opts);
69 push_category_table(&mut html, report, &anchors);
70 push_family_table(&mut html, report);
71 push_operations_table(&mut html, report, opts, &anchors);
72 if let Some(a) = audit {
73 push_spec_audit(&mut html, a);
74 }
75 html.push_str(FOOT);
76 html
77}
78
79fn compute_anchor_set(report: &SelfTestReport, opts: &RenderOptions) -> AnchorSet {
86 let mut missed: Vec<(&OperationResult, &CaseOutcome)> = Vec::new();
87 for op in &report.operations {
88 for neg in &op.negatives {
89 if !neg.passed {
90 missed.push((op, neg));
91 }
92 }
93 }
94 let take = opts.missed_cap.unwrap_or(usize::MAX);
95 let mut cats: std::collections::HashSet<String> = std::collections::HashSet::new();
96 let mut ops: std::collections::HashSet<String> = std::collections::HashSet::new();
97 for (op, neg) in missed.iter().take(take) {
98 let cat = neg.label.split(':').next().unwrap_or("other").to_string();
99 cats.insert(cat);
100 ops.insert(op_anchor_slug(&op.method, &op.path));
101 }
102 AnchorSet { cats, ops }
103}
104
105#[derive(Default)]
108struct AnchorSet {
109 cats: std::collections::HashSet<String>,
110 ops: std::collections::HashSet<String>,
111}
112
113const HEAD: &str = r#"<!doctype html>
115<html lang="en">
116<head>
117<meta charset="utf-8">
118<title>MockForge Conformance Report</title>
119<style>
120 body { font-family: -apple-system, system-ui, sans-serif; max-width: 1100px;
121 margin: 2rem auto; padding: 0 1rem; color: #1f2933; line-height: 1.5; }
122 h1 { font-size: 1.8rem; margin: 0 0 0.5rem; }
123 h2 { font-size: 1.3rem; margin: 2rem 0 0.5rem; border-bottom: 1px solid #d1d5db; padding-bottom: 0.3rem; }
124 .meta { color: #6b7280; font-size: 0.9rem; }
125 .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; margin: 1rem 0; }
126 .card { padding: 0.75rem 1rem; border-radius: 6px; background: #f3f4f6; }
127 .card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; }
128 .card .value { font-size: 1.6rem; font-weight: 600; color: #1f2933; }
129 .card.ok { background: #ecfdf5; } .card.ok .value { color: #047857; }
130 .card.warn { background: #fffbeb; } .card.warn .value { color: #b45309; }
131 .card.err { background: #fef2f2; } .card.err .value { color: #b91c1c; }
132 table { width: 100%; border-collapse: collapse; margin: 0.5rem 0 1.5rem; font-size: 0.9rem; }
133 th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e5e7eb; }
134 th { background: #f9fafb; font-weight: 600; color: #374151; }
135 tr:hover { background: #f9fafb; }
136 .badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 500; }
137 .badge.pass { background: #d1fae5; color: #047857; }
138 .badge.fail { background: #fee2e2; color: #b91c1c; }
139 .badge.info { background: #dbeafe; color: #1d4ed8; }
140 .badge.warn { background: #fef3c7; color: #92400e; }
141 .badge.err { background: #fee2e2; color: #b91c1c; }
142 .small { color: #6b7280; font-size: 0.85rem; }
143 code { background: #f3f4f6; padding: 0.05rem 0.3rem; border-radius: 3px; font-size: 0.9em; }
144</style>
145</head>
146<body>
147"#;
148
149const FOOT: &str = "\n</body>\n</html>\n";
150
151fn push_header(out: &mut String, _report: &SelfTestReport) {
152 out.push_str("<h1>MockForge Conformance Report</h1>\n");
153 out.push_str(
159 "<p class=\"meta\">Generated by <code>mockforge bench --conformance-self-test</code>. \
160 Probe-label reference: \
161 <a href=\"https://docs.mockforge.dev/reference/conformance-self-test-probes.html\">\
162 docs.mockforge.dev/reference/conformance-self-test-probes</a>.</p>\n",
163 );
164}
165
166fn push_summary_cards(out: &mut String, report: &SelfTestReport) {
167 let positives = report.positive_pass + report.positive_fail;
168 let neg_caught: usize = report.negative_caught.values().sum();
169 let neg_missed: usize = report.negative_missed.values().sum();
170 let pos_class = if report.positive_fail == 0 {
171 "ok"
172 } else {
173 "err"
174 };
175 let miss_class = if neg_missed == 0 { "ok" } else { "warn" };
176 out.push_str("<div class=\"cards\">\n");
177 push_card(out, "Positive cases", positives, pos_class);
178 push_card(out, "Positive failures", report.positive_fail, pos_class);
179 push_card(out, "Negatives matched (4xx)", neg_caught, "ok");
180 push_card(out, "Negatives mismatched (non-4xx)", neg_missed, miss_class);
181 push_card(out, "Operations", report.operations.len(), "");
182 out.push_str("</div>\n");
183}
184
185fn push_card(out: &mut String, label: &str, value: usize, class: &str) {
186 let class_attr = if class.is_empty() {
187 String::new()
188 } else {
189 format!(" {}", class)
190 };
191 out.push_str(&format!(
192 " <div class=\"card{class_attr}\"><div class=\"label\">{}</div><div class=\"value\">{}</div></div>\n",
193 html_escape(label),
194 value
195 ));
196}
197
198fn push_category_table(out: &mut String, report: &SelfTestReport, anchors: &AnchorSet) {
199 out.push_str("<h2>Negatives by category</h2>\n");
200 let mut keys: Vec<&String> =
201 report.negative_caught.keys().chain(report.negative_missed.keys()).collect();
202 keys.sort();
203 keys.dedup();
204 if keys.is_empty() {
205 out.push_str("<p class=\"small\">No negative probes ran — typically means no operations had any injectable surface.</p>\n");
206 return;
207 }
208 out.push_str("<table>\n<thead><tr><th>Category</th><th>Matched (4xx)</th><th>Mismatched (non-4xx)</th><th>Status</th></tr></thead>\n<tbody>\n");
209 for cat in keys {
210 let caught = report.negative_caught.get(cat).copied().unwrap_or(0);
211 let missed = report.negative_missed.get(cat).copied().unwrap_or(0);
212 let (badge_class, badge_text) = if missed == 0 {
219 ("pass", "PASS")
220 } else {
221 ("fail", "FAIL")
222 };
223 let missed_cell = if missed > 0 && anchors.cats.contains(cat) {
230 format!("<a href=\"#miss-cat-{}\">{}</a>", html_escape(cat), missed)
231 } else {
232 missed.to_string()
233 };
234 out.push_str(&format!(
235 "<tr><td><code>{}</code></td><td>{}</td><td>{}</td><td><span class=\"badge {}\">{}</span></td></tr>\n",
236 html_escape(cat),
237 caught,
238 missed_cell,
239 badge_class,
240 badge_text
241 ));
242 }
243 out.push_str("</tbody></table>\n");
244}
245
246fn push_operations_table(
247 out: &mut String,
248 report: &SelfTestReport,
249 opts: &RenderOptions,
250 anchors: &AnchorSet,
251) {
252 out.push_str("<h2>Per-operation results</h2>\n");
253 if report.operations.is_empty() {
254 out.push_str("<p class=\"small\">No operations.</p>\n");
255 return;
256 }
257 out.push_str("<table>\n<thead><tr><th>Method</th><th>Path</th><th>Positive</th><th>Matched / Mismatched</th><th>By category</th></tr></thead>\n<tbody>\n");
262 for op in &report.operations {
263 let pos_badge = match &op.positive {
264 Some(p) if p.passed => "<span class=\"badge pass\">2xx ✓</span>".to_string(),
265 Some(p) => format!("<span class=\"badge fail\">{} ✗</span>", p.actual_status),
266 None => "<span class=\"badge info\">none</span>".into(),
267 };
268 let (caught, missed) = op.negatives.iter().partition::<Vec<&CaseOutcome>, _>(|n| n.passed);
269 let op_slug = op_anchor_slug(&op.method, &op.path);
275 let missed_cell = if missed.is_empty() {
276 "0".to_string()
277 } else if anchors.ops.contains(&op_slug) {
278 format!("<a href=\"#miss-op-{}\">{}</a>", op_slug, missed.len())
279 } else {
280 missed.len().to_string()
281 };
282 let mut by_cat: BTreeMap<&str, usize> = BTreeMap::new();
286 for m in &missed {
287 let cat = m.label.split(':').next().unwrap_or("other");
288 *by_cat.entry(cat).or_insert(0) += 1;
289 }
290 let by_cat_cell = if by_cat.is_empty() {
291 String::new()
292 } else {
293 by_cat
294 .iter()
295 .map(|(cat, n)| format!("<code>{}:{}</code>", html_escape(cat), n))
296 .collect::<Vec<_>>()
297 .join(" ")
298 };
299 out.push_str(&format!(
300 "<tr><td><code>{}</code></td><td><code>{}</code></td><td>{}</td><td>{} / {}</td><td>{}</td></tr>\n",
301 html_escape(&op.method),
302 html_escape(&op.path),
303 pos_badge,
304 caught.len(),
305 missed_cell,
306 by_cat_cell
307 ));
308 }
309 out.push_str("</tbody></table>\n");
310 push_missed_detail(out, report, opts);
311}
312
313fn push_family_table(out: &mut String, report: &SelfTestReport) {
327 let families: &[(&str, &[&str])] = &[
328 ("Request body", &["request-body"]),
329 ("Parameters", &["parameters"]),
330 ("Security family", &["security", "owasp"]),
331 ];
332 if report.negative_caught.is_empty() && report.negative_missed.is_empty() {
334 return;
335 }
336 out.push_str("<h2>Negatives by category family</h2>\n");
337 out.push_str("<p class=\"small\">Rollup of related categories. The per-category breakdown above keeps the full resolution.</p>\n");
338 out.push_str("<table>\n<thead><tr><th>Family</th><th>Categories</th><th>Matched (4xx)</th><th>Mismatched (non-4xx)</th><th>Status</th></tr></thead>\n<tbody>\n");
339 for (family_name, members) in families {
340 let mut caught = 0;
341 let mut missed = 0;
342 let mut present: Vec<&str> = Vec::new();
343 for member in *members {
344 let m = (*member).to_string();
345 if let Some(c) = report.negative_caught.get(&m) {
346 caught += c;
347 present.push(*member);
348 }
349 if let Some(c) = report.negative_missed.get(&m) {
350 missed += c;
351 if !present.contains(member) {
352 present.push(*member);
353 }
354 }
355 }
356 if present.is_empty() {
357 continue;
358 }
359 let (badge_class, badge_text) = if missed == 0 {
360 ("pass", "PASS")
361 } else {
362 ("fail", "FAIL")
363 };
364 let member_codes = present
365 .iter()
366 .map(|m| format!("<code>{}</code>", html_escape(m)))
367 .collect::<Vec<_>>()
368 .join(" ");
369 out.push_str(&format!(
370 "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td><span class=\"badge {}\">{}</span></td></tr>\n",
371 html_escape(family_name),
372 member_codes,
373 caught,
374 missed,
375 badge_class,
376 badge_text
377 ));
378 }
379 out.push_str("</tbody></table>\n");
380}
381
382fn op_anchor_slug(method: &str, path: &str) -> String {
389 let mut s = format!("{method}_{path}");
390 s = s.to_ascii_lowercase();
391 s = s.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }).collect();
392 s
393}
394
395fn expected_status_label(case: &CaseOutcome) -> &'static str {
399 if case.expected_4xx {
400 "4xx (reject)"
401 } else {
402 "2xx-3xx (accept)"
403 }
404}
405
406fn push_missed_detail(out: &mut String, report: &SelfTestReport, opts: &RenderOptions) {
407 let mut missed: Vec<(&OperationResult, &CaseOutcome)> = Vec::new();
412 for op in &report.operations {
413 for neg in &op.negatives {
414 if !neg.passed {
415 missed.push((op, neg));
416 }
417 }
418 }
419 if missed.is_empty() {
420 return;
421 }
422 out.push_str(
423 "<h2>Mismatched negatives (server returned non-4xx to a probe expecting 4xx)</h2>\n",
424 );
425 let total = missed.len();
428 let cap_msg = match opts.missed_cap {
429 Some(cap) if total > cap => format!(
430 "{} mismatched negative(s). Showing first {} (raise with <code>--report-missed-cap N</code>, or <code>0</code> for no cap); full set in <code>conformance-self-test.json</code>.",
431 total, cap
432 ),
433 Some(_) => format!("{} mismatched negative(s). All shown.", total),
434 None => format!("{} mismatched negative(s). All shown (no cap).", total),
435 };
436 out.push_str(&format!("<p class=\"small\">{cap_msg}</p>\n"));
437 out.push_str("<table>\n<thead><tr><th>Method</th><th>Path</th><th>Label</th><th>Expected</th><th>Actual</th></tr></thead>\n<tbody>\n");
438 let take = opts.missed_cap.unwrap_or(usize::MAX);
439 let mut seen_cat: std::collections::HashSet<String> = std::collections::HashSet::new();
448 let mut seen_op: std::collections::HashSet<String> = std::collections::HashSet::new();
449 for (op, neg) in missed.iter().take(take) {
450 let cat = neg.label.split(':').next().unwrap_or("other").to_string();
451 let op_slug = op_anchor_slug(&op.method, &op.path);
452 let tr_id = if seen_cat.insert(cat.clone()) {
453 format!(" id=\"miss-cat-{}\"", html_escape(&cat))
454 } else {
455 String::new()
456 };
457 let op_anchor = if seen_op.insert(op_slug.clone()) {
458 format!("<span id=\"miss-op-{op_slug}\"></span>")
459 } else {
460 String::new()
461 };
462 out.push_str(&format!(
463 "<tr{}><td>{}<code>{}</code></td><td><code>{}</code></td><td><code>{}</code></td><td><span class=\"badge info\">{}</span></td><td>{}</td></tr>\n",
464 tr_id,
465 op_anchor,
466 html_escape(&op.method),
467 html_escape(&op.path),
468 html_escape(&neg.label),
469 expected_status_label(neg),
470 neg.actual_status
471 ));
472 }
473 out.push_str("</tbody></table>\n");
474}
475
476fn push_spec_audit(out: &mut String, audit: &serde_json::Value) {
477 out.push_str("<h2>Spec audit</h2>\n");
478 let findings = audit.get("findings").and_then(|v| v.as_array());
479 let coverage = audit.get("datatype_coverage").and_then(|v| v.as_object());
480 let ops = audit.get("operations_audited").and_then(|v| v.as_u64()).unwrap_or(0);
481 out.push_str(&format!(
482 "<p class=\"small\">Audited {ops} operation(s). Coverage map: {} datatype kind(s).</p>\n",
483 coverage.map(|c| c.len()).unwrap_or(0)
484 ));
485 if let Some(findings) = findings {
486 if findings.is_empty() {
487 out.push_str("<p class=\"small\">No findings.</p>\n");
488 } else {
489 let mut by_sev: BTreeMap<String, Vec<&serde_json::Value>> = BTreeMap::new();
491 for f in findings {
492 let sev = f.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
493 by_sev.entry(sev).or_default().push(f);
494 }
495 out.push_str("<table>\n<thead><tr><th>Severity</th><th>Category</th><th>Location</th><th>Message</th></tr></thead>\n<tbody>\n");
496 for (sev, items) in by_sev {
497 let badge_class = match sev.as_str() {
498 "error" => "err",
499 "warning" => "warn",
500 _ => "info",
501 };
502 for item in items {
503 let cat = item.get("category").and_then(|v| v.as_str()).unwrap_or("");
504 let loc = item.get("location").and_then(|v| v.as_str()).unwrap_or("");
505 let msg = item.get("message").and_then(|v| v.as_str()).unwrap_or("");
506 out.push_str(&format!(
507 "<tr><td><span class=\"badge {}\">{}</span></td><td><code>{}</code></td><td><code>{}</code></td><td>{}</td></tr>\n",
508 badge_class,
509 html_escape(&sev),
510 html_escape(cat),
511 html_escape(loc),
512 html_escape(msg)
513 ));
514 }
515 }
516 out.push_str("</tbody></table>\n");
517 }
518 }
519 if let Some(coverage) = coverage {
520 let mut entries: Vec<(&String, u64)> =
521 coverage.iter().filter_map(|(k, v)| v.as_u64().map(|c| (k, c))).collect();
522 entries.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
523 if !entries.is_empty() {
524 out.push_str("<h2>Datatype coverage</h2>\n");
525 out.push_str("<table>\n<thead><tr><th>Type</th><th>Count</th></tr></thead>\n<tbody>\n");
526 for (kind, count) in entries.iter().take(40) {
527 out.push_str(&format!(
528 "<tr><td><code>{}</code></td><td>{}</td></tr>\n",
529 html_escape(kind),
530 count
531 ));
532 }
533 out.push_str("</tbody></table>\n");
534 }
535 }
536}
537
538fn html_escape(s: &str) -> String {
539 let mut out = String::with_capacity(s.len());
540 for c in s.chars() {
541 match c {
542 '&' => out.push_str("&"),
543 '<' => out.push_str("<"),
544 '>' => out.push_str(">"),
545 '"' => out.push_str("""),
546 '\'' => out.push_str("'"),
547 _ => out.push(c),
548 }
549 }
550 out
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::conformance::self_test::{CaseOutcome, OperationResult, SelfTestReport};
557
558 fn sample_report() -> SelfTestReport {
559 SelfTestReport {
560 positive_pass: 3,
561 positive_fail: 1,
562 negative_caught: BTreeMap::from([("request-body".into(), 4), ("parameters".into(), 2)]),
563 negative_missed: BTreeMap::from([("owasp".into(), 1)]),
564 operations: vec![OperationResult {
565 method: "POST".into(),
566 path: "/users".into(),
567 positive: Some(CaseOutcome {
568 label: "positive".into(),
569 expected_4xx: false,
570 actual_status: 201,
571 passed: true,
572 }),
573 negatives: vec![CaseOutcome {
574 label: "owasp:sqli".into(),
575 expected_4xx: true,
576 actual_status: 200,
577 passed: false,
578 }],
579 }],
580 }
581 }
582
583 #[test]
584 fn html_contains_expected_sections() {
585 let html = render_html(&sample_report(), None);
586 assert!(html.contains("<title>MockForge Conformance Report</title>"));
587 assert!(html.contains("Positive cases"));
588 assert!(html.contains("Negatives by category"));
589 assert!(html.contains("Per-operation results"));
590 assert!(html.contains("Mismatched negatives"));
592 assert!(html.contains("request-body"));
594 assert!(html.contains("owasp:sqli"));
595 assert!(html.contains("/users"));
596 }
597
598 #[test]
599 fn html_renders_audit_section_when_present() {
600 let audit = serde_json::json!({
601 "findings": [
602 {"category": "servers", "severity": "warning",
603 "location": "#/servers", "message": "no servers declared"}
604 ],
605 "datatype_coverage": {"string": 5, "integer": 3},
606 "operations_audited": 7
607 });
608 let html = render_html(&sample_report(), Some(&audit));
609 assert!(html.contains("Spec audit"));
610 assert!(html.contains("no servers declared"));
611 assert!(html.contains("Datatype coverage"));
612 assert!(html.contains("string"));
613 assert!(html.contains("Audited 7 operation"));
614 }
615
616 #[test]
617 fn html_escapes_special_chars_in_labels() {
618 let mut report = sample_report();
619 report.operations[0].path = "/items/<script>".into();
620 report.operations[0].negatives[0].label = "owasp:xss:<>\"&".into();
621 let html = render_html(&report, None);
622 assert!(!html.contains("/items/<script>"));
624 assert!(html.contains("<script>"));
625 assert!(html.contains("""));
626 }
627
628 #[test]
629 fn html_handles_empty_report() {
630 let html = render_html(&SelfTestReport::default(), None);
631 assert!(html.contains("No negative probes ran"));
632 assert!(html.contains("No operations."));
633 }
634
635 #[test]
636 fn html_caps_missed_detail_at_default_200_rows() {
637 let mut report = SelfTestReport::default();
638 for i in 0..250 {
639 report.operations.push(OperationResult {
640 method: "GET".into(),
641 path: format!("/r/{i}"),
642 positive: None,
643 negatives: vec![CaseOutcome {
644 label: "parameters:missing-query".into(),
645 expected_4xx: true,
646 actual_status: 200,
647 passed: false,
648 }],
649 });
650 }
651 report.negative_missed.insert("parameters".into(), 250);
652 let html = render_html(&report, None);
653 assert!(html.contains("250 mismatched negative"));
655 assert!(html.contains("Showing first 200"));
656 assert!(html.contains("--report-missed-cap"));
657 }
658
659 #[test]
663 fn html_no_cap_shows_all_rows() {
664 let mut report = SelfTestReport::default();
665 for i in 0..50 {
666 report.operations.push(OperationResult {
667 method: "GET".into(),
668 path: format!("/r/{i}"),
669 positive: None,
670 negatives: vec![CaseOutcome {
671 label: "parameters:missing-query".into(),
672 expected_4xx: true,
673 actual_status: 200,
674 passed: false,
675 }],
676 });
677 }
678 let opts = RenderOptions { missed_cap: None };
679 let html = render_html_with_options(&report, None, &opts);
680 assert!(html.contains("50 mismatched negative"));
681 assert!(html.contains("All shown (no cap)"));
682 assert!(!html.contains("Showing first"));
683 }
684
685 #[test]
688 fn html_missed_table_has_expected_column() {
689 let mut report = sample_report();
690 report.operations[0].negatives = vec![CaseOutcome {
693 label: "security:bad-bearer".into(),
694 expected_4xx: true,
695 actual_status: 200,
696 passed: false,
697 }];
698 let html = render_html(&report, None);
699 assert!(html.contains("Expected"), "Expected column header missing");
700 assert!(
701 html.contains("4xx (reject)"),
702 "expected-status badge for negative probe missing"
703 );
704 }
705
706 #[test]
712 fn html_count_links_only_emit_for_visible_anchors() {
713 let mut report = SelfTestReport::default();
714 let cats = ["cat-a", "cat-b", "cat-a", "cat-b"];
720 for (i, c) in cats.iter().enumerate() {
721 report.operations.push(OperationResult {
722 method: "GET".into(),
723 path: format!("/r/{i}"),
724 positive: None,
725 negatives: vec![CaseOutcome {
726 label: format!("{c}:fail-{i}"),
727 expected_4xx: true,
728 actual_status: 200,
729 passed: false,
730 }],
731 });
732 *report.negative_missed.entry((*c).to_string()).or_insert(0) += 1;
733 }
734 let opts = RenderOptions {
735 missed_cap: Some(1),
736 };
737 let html = render_html_with_options(&report, None, &opts);
738 assert!(html.contains("id=\"miss-cat-cat-a\""));
741 assert!(!html.contains("id=\"miss-cat-cat-b\""));
742 assert!(html.contains("<a href=\"#miss-cat-cat-a\">"));
745 assert!(!html.contains("<a href=\"#miss-cat-cat-b\">"));
746 assert!(html.contains("<a href=\"#miss-op-get__r_0\">"));
749 assert!(!html.contains("<a href=\"#miss-op-get__r_2\">"));
750 }
751}