Repository navigation
fix(html2md): retain complex tables and referenced fragments #1336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
215ece4
fix(html2md): retain complex tables and referenced fragments
lanyue-llk d8369c6
test(markdown): validate native layout tables with their conversion c…
lanyue-llk bd5046f
test(fetch): bind layout table CLI acceptance to table conversion
lanyue-llk 2484ba0
fix(html2md): keep code and presentation semantics across output paths
lanyue-llk 467ddf8
fix(html2md): narrow table fallback to effective structural spans
lanyue-llk cc1131e
fix(html2md): preserve data structure without promoting layout tables
lanyue-llk db4d966
fix(html2md): preserve unique targets and source in HTML fallback
lanyue-llk a9f44c2
fix(html2md): share structural boundaries and preserve fallback targets
lanyue-llk 9cfff5c
test(fetch): enforce exact CLI output at the owning layer
lanyue-llk a6de081
fix(html2md): preserve source and structural boundary semantics
lanyue-llk 0801823
fix(html2md): finalize list spacing and retain code box boundaries
lanyue-llk e2f8b8e
fix(html2md): align HTML fallback with current DOM helpers
ldm0 954cb4a
fix(html2md): keep fragment targets inside author links
ldm0 809360a
fix(html2md): preserve author links around media in HTML fallback
ldm0 6dc33f3
fix(html2md): share structural block boundaries across output paths
ldm0 e3ae2c8
fix(html2md): distinguish readable content from empty markup
ldm0 e5420d2
fix(html2md): retain IDs referenced by table headers
ldm0 88ad627
perf(html2md): collect fragment targets in linear time
ldm0 2dfe153
fix(html2md): expand presentational tables in HTML fallback
ldm0 ebec9cf
fix(html2md): preserve skipped section targets in GFM tables
ldm0 1cf5f44
fix(html2md): separate adjacent input values in HTML fallback
ldm0 f67f3d2
fix(html2md): preserve open state in HTML fallback
ldm0 38253bb
fix(html2md): inherit link context for inline HTML fallback
ldm0 499abad
fix(html2md): align whitespace-only code content across output paths
ldm0 6217653
fix(html2md): filter serialized state before fallback dispatch
ldm0 6cbdfae
fix(html2md): replace invalid UTF-8 in fragment decoding
ldm0 eaf1149
fix(html2md): keep accessible names out of code text
ldm0 0381786
fix(html2md): retain author links around block HTML
ldm0 7e147d3
fix(html2md): retain referenced targets on expanded elements
ldm0 ad09837
fix(html2md): preserve table column group structure
ldm0 3bbe9a7
fix(html2md): retain boundaries of expanded block elements
ldm0 ea6df48
fix(html2md): preserve strike elements in HTML fallback
ldm0 818cf96
fix(html2md): escape generated text in inline HTML code
ldm0 72b9959
fix(html2md): normalize line endings in preformatted inline code
ldm0 034ee80
fix(html2md): encode pipes in raw HTML attributes
ldm0 0cc2750
fix(html2md): preserve outer formatting around block HTML
ldm0 9328db9
fix(html2md): break CRLF continuity at MathML content
ldm0 0d86fe2
fix(html2md): preserve media links inside placeholder anchors
ldm0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| use std::collections::HashSet; | ||
|
|
||
| use crate::{Dom, NodeKind}; | ||
|
|
||
| pub(crate) struct Targets { | ||
| referenced: HashSet<String>, | ||
| header_ids: HashSet<String>, | ||
| ids: HashSet<String>, | ||
| } | ||
|
|
||
| impl Targets { | ||
| pub(crate) fn is_empty(&self) -> bool { | ||
| self.referenced.is_empty() && self.header_ids.is_empty() | ||
| } | ||
|
|
||
| pub(crate) fn targets<'a, D: Dom + ?Sized>(&self, dom: &'a D, node: D::NodeId) -> Vec<&'a str> { | ||
| let mut targets = Vec::new(); | ||
| if let Some(id) = dom.attribute(node, "id").filter(|id| { | ||
| self.referenced.contains(*id) | ||
| || (dom.node_kind(node) == NodeKind::Element("th") && self.header_ids.contains(*id)) | ||
| }) { | ||
| targets.push(id); | ||
| } | ||
| if dom.node_kind(node) == NodeKind::Element("a") | ||
| && let Some(name) = dom | ||
| .attribute(node, "name") | ||
| .filter(|name| self.referenced.contains(*name) && !self.ids.contains(*name)) | ||
| && !targets.contains(&name) | ||
| { | ||
| targets.push(name); | ||
| } | ||
| targets | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn referenced<D: Dom + ?Sized>(dom: &D, root: D::NodeId, limit: usize) -> Targets { | ||
| let mut available = HashSet::new(); | ||
| let mut element_ids = HashSet::new(); | ||
| let mut header_ids = HashSet::new(); | ||
| let mut fragments = Vec::new(); | ||
| let mut headers = HashSet::new(); | ||
| let fragment_links = dom.may_have_fragment_links(); | ||
| walk(dom, root, limit, |node| { | ||
| if fragment_links | ||
| && let Some(fragment) = dom | ||
| .attribute(node, "href") | ||
| .and_then(|href| href.strip_prefix('#')) | ||
| .filter(|fragment| !fragment.is_empty()) | ||
| { | ||
| fragments.push(fragment); | ||
| } | ||
| if matches!(dom.node_kind(node), NodeKind::Element("td" | "th")) | ||
| && let Some(value) = dom.attribute(node, "headers") | ||
| { | ||
| headers.extend( | ||
| value | ||
| .split(crate::writer::is_space) | ||
| .filter(|id| !id.is_empty()), | ||
| ); | ||
| } | ||
| false | ||
| }); | ||
| if fragments.is_empty() && headers.is_empty() { | ||
| return Targets { | ||
| referenced: available, | ||
| header_ids, | ||
| ids: element_ids, | ||
| }; | ||
| } | ||
| walk(dom, root, limit, |node| { | ||
| for attribute in ["id", "name"] { | ||
| if attribute == "name" && dom.node_kind(node) != NodeKind::Element("a") { | ||
| continue; | ||
| } | ||
| if let Some(value) = dom.attribute(node, attribute) { | ||
| available.insert(value.to_owned()); | ||
| if attribute == "id" { | ||
| element_ids.insert(value.to_owned()); | ||
| if dom.node_kind(node) == NodeKind::Element("th") && headers.contains(value) { | ||
| header_ids.insert(value.to_owned()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| false | ||
| }); | ||
| let mut referenced = HashSet::new(); | ||
| for fragment in fragments { | ||
| if available.contains(fragment) { | ||
| referenced.insert(fragment.to_owned()); | ||
| } else { | ||
| let decoded = decode_fragment(fragment); | ||
| if available.contains(&decoded) { | ||
| referenced.insert(decoded); | ||
| } | ||
| } | ||
| } | ||
| Targets { | ||
| referenced, | ||
| header_ids, | ||
| ids: element_ids, | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn contains<D: Dom + ?Sized>( | ||
| dom: &D, | ||
| root: D::NodeId, | ||
| limit: usize, | ||
| ids: &Targets, | ||
| ) -> bool { | ||
| walk(dom, root, limit, |node| !ids.targets(dom, node).is_empty()) | ||
| } | ||
|
|
||
| pub(crate) fn within<D: Dom + ?Sized>( | ||
| dom: &D, | ||
| root: D::NodeId, | ||
| limit: usize, | ||
| ids: &Targets, | ||
| ) -> Vec<String> { | ||
| let mut found = Vec::new(); | ||
| walk(dom, root, limit, |node| { | ||
| for id in ids.targets(dom, node) { | ||
| // Every caller deduplicates with its shared emitted-target set. | ||
| found.push(id.to_owned()); | ||
| } | ||
| false | ||
| }); | ||
| found | ||
| } | ||
|
|
||
| fn walk<D: Dom + ?Sized>( | ||
| dom: &D, | ||
| root: D::NodeId, | ||
| limit: usize, | ||
| mut found: impl FnMut(D::NodeId) -> bool, | ||
| ) -> bool { | ||
| let mut stack = vec![(Some(root), 0, false)]; | ||
| while let Some((node, depth, siblings)) = stack.pop() { | ||
| let Some(node) = node else { | ||
| continue; | ||
| }; | ||
| if siblings { | ||
| stack.push((dom.next_sibling(node), depth, true)); | ||
| } | ||
| if depth >= limit { | ||
| continue; | ||
| } | ||
| if matches!( | ||
| dom.node_kind(node), | ||
| NodeKind::Other | ||
| | NodeKind::Element("head" | "script" | "style" | "noscript" | "template") | ||
| ) { | ||
| continue; | ||
| } | ||
| if found(node) { | ||
| return true; | ||
| } | ||
| stack.push((dom.first_child(node), depth + 1, true)); | ||
| } | ||
| false | ||
| } | ||
|
|
||
| pub(crate) fn decode_fragment(fragment: &str) -> String { | ||
| let bytes = fragment.as_bytes(); | ||
| let mut decoded = Vec::with_capacity(bytes.len()); | ||
| let mut offset = 0; | ||
| while offset < bytes.len() { | ||
| if bytes[offset] == b'%' && offset + 2 < bytes.len() { | ||
| let hex = |byte: u8| (byte as char).to_digit(16); | ||
| if let (Some(high), Some(low)) = (hex(bytes[offset + 1]), hex(bytes[offset + 2])) { | ||
| decoded.push((high * 16 + low) as u8); | ||
| offset += 3; | ||
| continue; | ||
| } | ||
| } | ||
| decoded.push(bytes[offset]); | ||
| offset += 1; | ||
| } | ||
| String::from_utf8(decoded) | ||
| .unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned()) | ||
| } | ||
|
|
||
| pub(crate) fn markup(id: &str) -> String { | ||
| let mut result = String::from("<span id=\""); | ||
| for character in id.chars() { | ||
| match character { | ||
| '&' => result.push_str("&"), | ||
| '"' => result.push_str("""), | ||
| '<' => result.push_str("<"), | ||
| '>' => result.push_str(">"), | ||
| '\n' => result.push_str(" "), | ||
| '\r' => result.push_str(" "), | ||
| '|' => result.push_str("|"), | ||
| character => result.push(character), | ||
| } | ||
| } | ||
| result.push_str("\"></span>"); | ||
| result | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.