@@ -14,6 +14,43 @@ use log::warn;
1414
1515pub 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 ) ]
3679pub 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+ /// ```
14461499pub 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> {
15821634impl 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