Skip to content

Commit 06ac3a0

Browse files
committed
refactor: api improvements and safer public interface
1 parent cfc8166 commit 06ac3a0

3 files changed

Lines changed: 125 additions & 90 deletions

File tree

litehtml/examples/render.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ fn main() {
2727
// Layout the document to get its actual content height
2828
let mut container = PixbufContainer::new(width, win_height);
2929
let content_height = {
30-
if let Some(mut doc) = Document::from_html(&html, &mut container, None, None) {
31-
doc.render(width as f32);
30+
if let Ok(mut doc) = Document::from_html(&html, &mut container, None, None) {
31+
let _ = doc.render(width as f32);
3232
(doc.height().ceil() as u32).max(win_height)
3333
} else {
3434
win_height
@@ -37,8 +37,8 @@ fn main() {
3737

3838
// Re-create container at full content height and render
3939
container.resize(width, content_height);
40-
if let Some(mut doc) = Document::from_html(&html, &mut container, None, None) {
41-
doc.render(width as f32);
40+
if let Ok(mut doc) = Document::from_html(&html, &mut container, None, None) {
41+
let _ = doc.render(width as f32);
4242
doc.draw(
4343
0,
4444
0.0,

litehtml/src/lib.rs

Lines changed: 119 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ use log::warn;
1414

1515
pub use litehtml_sys as sys;
1616

17+
// ---------------------------------------------------------------------------
18+
// Error types
19+
// ---------------------------------------------------------------------------
20+
21+
/// Error returned by [`Document::from_html`] when document creation fails.
22+
#[derive(Debug)]
23+
pub enum CreateError {
24+
/// The input HTML or CSS string contained an interior null byte.
25+
InvalidString(std::ffi::NulError),
26+
/// The litehtml C++ engine returned a null document pointer.
27+
CreateFailed,
28+
}
29+
30+
impl std::fmt::Display for CreateError {
31+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32+
match self {
33+
Self::InvalidString(e) => write!(f, "string contains interior null byte: {e}"),
34+
Self::CreateFailed => write!(f, "litehtml failed to create document"),
35+
}
36+
}
37+
}
38+
39+
impl std::error::Error for CreateError {
40+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41+
match self {
42+
Self::InvalidString(e) => Some(e),
43+
Self::CreateFailed => None,
44+
}
45+
}
46+
}
47+
48+
impl From<std::ffi::NulError> for CreateError {
49+
fn from(e: std::ffi::NulError) -> Self {
50+
Self::InvalidString(e)
51+
}
52+
}
53+
1754
// ---------------------------------------------------------------------------
1855
// Safe Rust value types
1956
// ---------------------------------------------------------------------------
@@ -32,6 +69,12 @@ pub struct Size {
3269
pub height: f32,
3370
}
3471

72+
/// RGBA color value.
73+
///
74+
/// Note: litehtml's C++ `web_color` type carries an `is_current_color` flag
75+
/// (for the CSS `currentColor` keyword). This flag is **not** preserved here
76+
/// because litehtml resolves `currentColor` to a concrete RGBA value during
77+
/// CSS property computation, before passing colors to container callbacks.
3578
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3679
pub struct Color {
3780
pub r: u8,
@@ -857,7 +900,7 @@ pub trait DocumentContainer {
857900
}
858901

859902
/// Draw a list item marker (bullet, number, etc.).
860-
fn draw_list_marker(&mut self, hdc: usize, marker: &ListMarker);
903+
fn draw_list_marker(&mut self, hdc: usize, marker: &ListMarker) {}
861904

862905
/// Notify the container that an image should be loaded.
863906
fn load_image(&mut self, src: &str, baseurl: &str, redraw_on_ready: bool);
@@ -896,25 +939,25 @@ pub trait DocumentContainer {
896939
);
897940

898941
/// Draw element borders.
899-
fn draw_borders(&mut self, hdc: usize, borders: &Borders, draw_pos: Position, root: bool);
942+
fn draw_borders(&mut self, hdc: usize, borders: &Borders, draw_pos: Position, root: bool) {}
900943

901944
/// Set the document title.
902-
fn set_caption(&mut self, caption: &str);
945+
fn set_caption(&mut self, caption: &str) {}
903946

904947
/// Set the document base URL.
905-
fn set_base_url(&mut self, base_url: &str);
948+
fn set_base_url(&mut self, base_url: &str) {}
906949

907950
/// Called when a `<link>` element is encountered.
908951
fn link(&mut self) {}
909952

910953
/// Called when the user clicks an anchor element.
911-
fn on_anchor_click(&mut self, url: &str);
954+
fn on_anchor_click(&mut self, url: &str) {}
912955

913956
/// Called on mouse enter/leave events.
914957
fn on_mouse_event(&mut self, event: MouseEvent) {}
915958

916959
/// Set the mouse cursor style.
917-
fn set_cursor(&mut self, cursor: &str);
960+
fn set_cursor(&mut self, cursor: &str) {}
918961

919962
/// Transform text according to CSS `text-transform`. Returns the
920963
/// transformed string.
@@ -928,10 +971,10 @@ pub trait DocumentContainer {
928971
}
929972

930973
/// Push a clipping rectangle onto the clip stack.
931-
fn set_clip(&mut self, pos: Position, radius: BorderRadiuses);
974+
fn set_clip(&mut self, pos: Position, radius: BorderRadiuses) {}
932975

933976
/// Pop the most recent clipping rectangle.
934-
fn del_clip(&mut self);
977+
fn del_clip(&mut self) {}
935978

936979
/// Return the current viewport rectangle.
937980
fn get_viewport(&self) -> Position;
@@ -1399,40 +1442,40 @@ unsafe extern "C" fn cb_get_language(
13991442
}));
14001443
}
14011444

1402-
/// Build a fully populated vtable pointing to the callback bridge functions.
1403-
fn build_vtable() -> sys::lh_container_vtable_t {
1404-
sys::lh_container_vtable_t {
1405-
create_font: Some(cb_create_font),
1406-
delete_font: Some(cb_delete_font),
1407-
text_width: Some(cb_text_width),
1408-
draw_text: Some(cb_draw_text),
1409-
pt_to_px: Some(cb_pt_to_px),
1410-
get_default_font_size: Some(cb_get_default_font_size),
1411-
get_default_font_name: Some(cb_get_default_font_name),
1412-
draw_list_marker: Some(cb_draw_list_marker),
1413-
load_image: Some(cb_load_image),
1414-
get_image_size: Some(cb_get_image_size),
1415-
draw_image: Some(cb_draw_image),
1416-
draw_solid_fill: Some(cb_draw_solid_fill),
1417-
draw_linear_gradient: Some(cb_draw_linear_gradient),
1418-
draw_radial_gradient: Some(cb_draw_radial_gradient),
1419-
draw_conic_gradient: Some(cb_draw_conic_gradient),
1420-
draw_borders: Some(cb_draw_borders),
1421-
set_caption: Some(cb_set_caption),
1422-
set_base_url: Some(cb_set_base_url),
1423-
link: Some(cb_link),
1424-
on_anchor_click: Some(cb_on_anchor_click),
1425-
on_mouse_event: Some(cb_on_mouse_event),
1426-
set_cursor: Some(cb_set_cursor),
1427-
transform_text: Some(cb_transform_text),
1428-
import_css: Some(cb_import_css),
1429-
set_clip: Some(cb_set_clip),
1430-
del_clip: Some(cb_del_clip),
1431-
get_viewport: Some(cb_get_viewport),
1432-
get_media_features: Some(cb_get_media_features),
1433-
get_language: Some(cb_get_language),
1434-
}
1435-
}
1445+
/// Single shared vtable for all Document instances. Every field is a
1446+
/// compile-time-constant function pointer; per-document state is carried
1447+
/// through `user_data`, not the vtable.
1448+
static CONTAINER_VTABLE: sys::lh_container_vtable_t = sys::lh_container_vtable_t {
1449+
create_font: Some(cb_create_font),
1450+
delete_font: Some(cb_delete_font),
1451+
text_width: Some(cb_text_width),
1452+
draw_text: Some(cb_draw_text),
1453+
pt_to_px: Some(cb_pt_to_px),
1454+
get_default_font_size: Some(cb_get_default_font_size),
1455+
get_default_font_name: Some(cb_get_default_font_name),
1456+
draw_list_marker: Some(cb_draw_list_marker),
1457+
load_image: Some(cb_load_image),
1458+
get_image_size: Some(cb_get_image_size),
1459+
draw_image: Some(cb_draw_image),
1460+
draw_solid_fill: Some(cb_draw_solid_fill),
1461+
draw_linear_gradient: Some(cb_draw_linear_gradient),
1462+
draw_radial_gradient: Some(cb_draw_radial_gradient),
1463+
draw_conic_gradient: Some(cb_draw_conic_gradient),
1464+
draw_borders: Some(cb_draw_borders),
1465+
set_caption: Some(cb_set_caption),
1466+
set_base_url: Some(cb_set_base_url),
1467+
link: Some(cb_link),
1468+
on_anchor_click: Some(cb_on_anchor_click),
1469+
on_mouse_event: Some(cb_on_mouse_event),
1470+
set_cursor: Some(cb_set_cursor),
1471+
transform_text: Some(cb_transform_text),
1472+
import_css: Some(cb_import_css),
1473+
set_clip: Some(cb_set_clip),
1474+
del_clip: Some(cb_del_clip),
1475+
get_viewport: Some(cb_get_viewport),
1476+
get_media_features: Some(cb_get_media_features),
1477+
get_language: Some(cb_get_language),
1478+
};
14361479

14371480
// ---------------------------------------------------------------------------
14381481
// Document
@@ -1443,10 +1486,18 @@ fn build_vtable() -> sys::lh_container_vtable_t {
14431486
///
14441487
/// `Document` is intentionally `!Send` and `!Sync` because the underlying
14451488
/// C++ engine is single-threaded.
1489+
///
1490+
/// ```compile_fail
1491+
/// fn assert_send<T: Send>() {}
1492+
/// assert_send::<litehtml::Document<'static>>();
1493+
/// ```
1494+
///
1495+
/// ```compile_fail
1496+
/// fn assert_sync<T: Sync>() {}
1497+
/// assert_sync::<litehtml::Document<'static>>();
1498+
/// ```
14461499
pub struct Document<'a> {
14471500
raw: *mut sys::lh_document_t,
1448-
/// Kept alive so the vtable pointer inside litehtml remains valid.
1449-
vtable: *mut sys::lh_container_vtable_t,
14501501
/// Kept alive so the user_data pointer inside litehtml remains valid.
14511502
bridge: *mut BridgeData<'a>,
14521503
}
@@ -1455,22 +1506,23 @@ impl<'a> Document<'a> {
14551506
/// Parse HTML into a document, using `container` for all rendering
14561507
/// callbacks.
14571508
///
1458-
/// Returns `None` if litehtml fails to create the document (e.g.
1459-
/// completely empty input after null-termination issues).
1509+
/// Returns an error if the input strings contain interior null bytes
1510+
/// or if litehtml fails to create the document.
14601511
///
14611512
/// `master_css` is the user-agent stylesheet applied before any document
14621513
/// styles. `user_styles` are additional CSS rules applied after the
14631514
/// document styles.
1515+
#[must_use = "document must be stored for rendering"]
14641516
pub fn from_html(
14651517
html: &str,
14661518
container: &'a mut dyn DocumentContainer,
14671519
master_css: Option<&str>,
14681520
user_styles: Option<&str>,
1469-
) -> Option<Self> {
1470-
let c_html = CString::new(html).ok()?;
1521+
) -> Result<Self, CreateError> {
1522+
let c_html = CString::new(html)?;
14711523

1472-
let c_master_css = master_css.and_then(|s| CString::new(s).ok());
1473-
let c_user_styles = user_styles.and_then(|s| CString::new(s).ok());
1524+
let c_master_css = master_css.map(CString::new).transpose()?;
1525+
let c_user_styles = user_styles.map(CString::new).transpose()?;
14741526

14751527
let master_css_ptr = c_master_css
14761528
.as_ref()
@@ -1491,8 +1543,10 @@ impl<'a> Document<'a> {
14911543
};
14921544
let bridge_ptr = Box::into_raw(Box::new(bridge_data));
14931545

1494-
let vtable = build_vtable();
1495-
let vtable_ptr = Box::into_raw(Box::new(vtable));
1546+
// SAFETY: the C++ side only reads through this pointer (never writes).
1547+
// See CDocumentContainer in litehtml_c.cpp — all vtable access is read-only.
1548+
let vtable_ptr =
1549+
std::ptr::addr_of!(CONTAINER_VTABLE) as *mut sys::lh_container_vtable_t;
14961550

14971551
let raw = unsafe {
14981552
sys::lh_document_create_from_string(
@@ -1505,23 +1559,21 @@ impl<'a> Document<'a> {
15051559
};
15061560

15071561
if raw.is_null() {
1508-
// Clean up allocations on failure
15091562
unsafe {
1510-
drop(Box::from_raw(vtable_ptr));
15111563
drop(Box::from_raw(bridge_ptr));
15121564
}
1513-
return None;
1565+
return Err(CreateError::CreateFailed);
15141566
}
15151567

1516-
Some(Self {
1568+
Ok(Self {
15171569
raw,
1518-
vtable: vtable_ptr,
15191570
bridge: bridge_ptr,
15201571
})
15211572
}
15221573

15231574
/// Lay out the document within `max_width` pixels. Returns the actual
15241575
/// content width after layout.
1576+
#[must_use = "returns the content width after layout"]
15251577
pub fn render(&mut self, max_width: f32) -> f32 {
15261578
unsafe { sys::lh_document_render(self.raw, max_width) }
15271579
}
@@ -1582,11 +1634,9 @@ impl<'a> Document<'a> {
15821634
impl Drop for Document<'_> {
15831635
fn drop(&mut self) {
15841636
unsafe {
1637+
// Destroy the document first (it may call callbacks during teardown),
1638+
// then free the bridge data.
15851639
sys::lh_document_destroy(self.raw);
1586-
// Free the vtable and bridge allocations. Order matters: destroy
1587-
// the document first (it may call callbacks during teardown),
1588-
// then free the support structures.
1589-
drop(Box::from_raw(self.vtable));
15901640
drop(Box::from_raw(self.bridge));
15911641
}
15921642
}
@@ -1748,15 +1798,15 @@ mod tests {
17481798
fn test_parse_simple_html() {
17491799
let mut container = TestContainer::new();
17501800
let doc = Document::from_html("<p>Hello</p>", &mut container, None, None);
1751-
assert!(doc.is_some());
1801+
assert!(doc.is_ok());
17521802
}
17531803

17541804
#[test]
17551805
fn test_render_dimensions() {
17561806
let mut container = TestContainer::new();
17571807
let mut doc =
17581808
Document::from_html("<p>Hello world</p>", &mut container, None, None).unwrap();
1759-
doc.render(800.0);
1809+
let _ = doc.render(800.0);
17601810
assert!(doc.width() > 0.0);
17611811
assert!(doc.height() > 0.0);
17621812
}
@@ -1767,7 +1817,7 @@ mod tests {
17671817
let mut doc =
17681818
Document::from_html("<h1>Title</h1><p>Body text</p>", &mut container, None, None)
17691819
.unwrap();
1770-
doc.render(800.0);
1820+
let _ = doc.render(800.0);
17711821
let clip = Position {
17721822
x: 0.0,
17731823
y: 0.0,
@@ -1782,7 +1832,7 @@ mod tests {
17821832
let mut container = TestContainer::new();
17831833
let mut doc =
17841834
Document::from_html("<a href=\"#\">Link</a>", &mut container, None, None).unwrap();
1785-
doc.render(800.0);
1835+
let _ = doc.render(800.0);
17861836
let _ = doc.on_mouse_over(10.0, 10.0, 10.0, 10.0);
17871837
let _ = doc.on_lbutton_down(10.0, 10.0, 10.0, 10.0);
17881838
let _ = doc.on_lbutton_up(10.0, 10.0, 10.0, 10.0);
@@ -1793,7 +1843,7 @@ mod tests {
17931843
fn test_media_changed() {
17941844
let mut container = TestContainer::new();
17951845
let mut doc = Document::from_html("<p>Test</p>", &mut container, None, None).unwrap();
1796-
doc.render(800.0);
1846+
let _ = doc.render(800.0);
17971847
let _ = doc.media_changed();
17981848
}
17991849

@@ -1802,15 +1852,15 @@ mod tests {
18021852
let mut container = TestContainer::new();
18031853
let css = "body { margin: 0; padding: 0; }";
18041854
let doc = Document::from_html("<p>Styled</p>", &mut container, Some(css), None);
1805-
assert!(doc.is_some());
1855+
assert!(doc.is_ok());
18061856
}
18071857

18081858
#[test]
18091859
fn test_with_user_styles() {
18101860
let mut container = TestContainer::new();
18111861
let user_css = "p { color: red; }";
18121862
let doc = Document::from_html("<p>Red</p>", &mut container, None, Some(user_css));
1813-
assert!(doc.is_some());
1863+
assert!(doc.is_ok());
18141864
}
18151865

18161866
#[test]
@@ -1881,22 +1931,7 @@ mod tests {
18811931
assert_eq!(m, back);
18821932
}
18831933

1884-
#[test]
1885-
fn test_not_send_sync() {
1886-
// Compile-time assertion: Document should not be Send or Sync.
1887-
fn assert_not_send<T: Send>() {}
1888-
fn assert_not_sync<T: Sync>() {}
1889-
1890-
// These would fail to compile if uncommented, proving Document is
1891-
// !Send and !Sync:
1892-
// assert_not_send::<Document>();
1893-
// assert_not_sync::<Document>();
1894-
1895-
// Instead, we just verify the raw pointer fields exist (which is
1896-
// what prevents auto-impl of Send/Sync).
1897-
let _ = assert_not_send::<i32>;
1898-
let _ = assert_not_sync::<i32>;
1899-
}
1934+
// test_not_send_sync is now a compile_fail doc test on the Document struct.
19001935

19011936
#[cfg(feature = "pixbuf")]
19021937
mod pixbuf_tests {

0 commit comments

Comments
 (0)