Files
zed/crates/repl/src/outputs/html.rs
Mohamad Khani b72a46db68
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 02:22:17 +03:30

199 lines
6.2 KiB
Rust

use anyhow::Result;
use html_to_markdown::markdown::{
CodeHandler, HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler,
WebpageChromeRemover,
};
use html_to_markdown::{TagHandler, convert_html_to_markdown};
use std::cell::RefCell;
use std::rc::Rc;
/// Convert HTML to Markdown for rendering in the REPL.
pub fn html_to_markdown(html: &str) -> Result<String> {
let mut handlers: Vec<TagHandler> = vec![
// WebpageChromeRemover must come first to skip style, script, head, nav tags
Rc::new(RefCell::new(WebpageChromeRemover)),
Rc::new(RefCell::new(ParagraphHandler)),
Rc::new(RefCell::new(HeadingHandler)),
Rc::new(RefCell::new(ListHandler)),
Rc::new(RefCell::new(TableHandler::new())),
Rc::new(RefCell::new(StyledTextHandler)),
Rc::new(RefCell::new(CodeHandler)),
];
let markdown = convert_html_to_markdown(html.as_bytes(), &mut handlers)?;
Ok(clean_markdown_tables(&markdown))
}
/// Clean up markdown table formatting and ensure tables have separator rows.
fn clean_markdown_tables(markdown: &str) -> String {
let lines: Vec<&str> = markdown.lines().collect();
let mut result: Vec<String> = Vec::new();
let mut in_table = false;
let mut has_separator = false;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with('|') {
let normalized = normalize_table_row(trimmed);
if !in_table {
// Starting a new table
in_table = true;
has_separator = false;
}
// Check if this line is a separator row
if trimmed.contains("---") {
has_separator = true;
}
result.push(normalized.clone());
// If this is the first row and no separator exists yet,
// check if next row is a table row (not separator) and add one
if !has_separator {
let next_is_table_row = i + 1 < lines.len()
&& lines[i + 1].trim().starts_with('|')
&& !lines[i + 1].contains("---");
if next_is_table_row {
// Insert separator after first row
let col_count = normalized.matches('|').count().saturating_sub(1);
if col_count > 0 {
let separator = (0..col_count)
.map(|_| "---")
.collect::<Vec<_>>()
.join(" | ");
result.push(format!("| {} |", separator));
has_separator = true;
}
}
}
} else {
// Not a table row
if !trimmed.is_empty() {
result.push(trimmed.to_string());
}
in_table = false;
has_separator = false;
}
}
result.join("\n")
}
/// Normalize a table row by trimming cells and ensuring consistent spacing.
fn normalize_table_row(row: &str) -> String {
let parts: Vec<&str> = row.split('|').collect();
let normalized: Vec<String> = parts.iter().map(|cell| cell.trim().to_string()).collect();
normalized.join(" | ").trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_table_to_markdown() {
let html = r#"<table>
<thead><tr><th>A</th><th>B</th></tr></thead>
<tbody><tr><td>1</td><td>x</td></tr></tbody>
</table>"#;
let md = html_to_markdown(html).unwrap();
assert!(md.contains("|"));
assert!(md.contains("---"));
}
#[test]
fn test_html_with_headings() {
let html = "<h1>Title</h1><p>Content</p>";
let md = html_to_markdown(html).unwrap();
assert!(md.contains("# Title"));
}
#[test]
fn test_pandas_dataframe_html() {
let html = r#"<table border="1" class="dataframe">
<thead><tr><th></th><th>A</th><th>B</th></tr></thead>
<tbody>
<tr><th>0</th><td>1</td><td>x</td></tr>
<tr><th>1</th><td>2</td><td>y</td></tr>
</tbody>
</table>"#;
let md = html_to_markdown(html).unwrap();
assert!(md.contains("|"));
// Verify table rows are properly formatted (start with |)
for line in md.lines() {
if line.contains("|") {
assert!(
line.starts_with("|"),
"Table line should start with |: {:?}",
line
);
}
}
}
#[test]
fn test_table_format_normalized() {
let html = r#"<table>
<thead>
<tr><th>Name</th><th>Age</th></tr>
</thead>
<tbody>
<tr><td>Alice</td><td>25</td></tr>
</tbody>
</table>"#;
let md = html_to_markdown(html).unwrap();
// Should have clean table format
assert!(md.contains("| Name | Age |"));
assert!(md.contains("| --- | --- |"));
assert!(md.contains("| Alice | 25 |"));
}
#[test]
fn test_style_tags_are_filtered() {
let html = r#"<style>
.dataframe { border: 1px solid; }
</style>
<table>
<thead><tr><th>A</th></tr></thead>
<tbody><tr><td>1</td></tr></tbody>
</table>"#;
let md = html_to_markdown(html).unwrap();
// Style content should not appear in output
assert!(!md.contains("dataframe"));
assert!(!md.contains("border"));
// Table should still be present
assert!(md.contains("| A |"));
}
#[test]
fn test_table_without_thead() {
// Tables without <thead> should still get a separator row
let html = r#"<table>
<tr><th>Feature</th><th>Supported</th></tr>
<tr><td>Tables</td><td>✓</td></tr>
<tr><td>Lists</td><td>✓</td></tr>
</table>"#;
let md = html_to_markdown(html).unwrap();
// Should have separator row inserted after first row
assert!(
md.contains("| --- | --- |"),
"Missing separator row: {}",
md
);
assert!(md.contains("| Feature | Supported |"));
assert!(md.contains("| Tables | ✓ |"));
}
}