wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-canvas/src/lib.rs@2b84405277e54ab809e328cf0237374d4b4dbd0c

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1#![deny(rust_2018_idioms, unsafe_code)]
2#![warn(
3    absolute_paths_not_starting_with_crate,
4    ambiguous_associated_items,
5    anonymous_parameters,
6    arithmetic_overflow,
7    array_into_iter,
8    asm_sub_register,
9    bad_asm_style,
10    bindings_with_variant_name,
11    break_with_label_and_loop,
12    clashing_extern_declarations,
13    coherence_leak_check,
14    conflicting_repr_hints,
15    confusable_idents,
16    const_evaluatable_unchecked,
17    const_item_mutation,
18    dangling_pointers_from_temporaries,
19    dead_code,
20    deprecated_in_future,
21    deprecated_where_clause_location,
22    deprecated,
23    deref_into_dyn_supertrait,
24    deref_nullptr,
25    drop_bounds,
26    duplicate_macro_attributes,
27    dyn_drop,
28    ellipsis_inclusive_range_patterns,
29    enum_intrinsics_non_enums,
30    explicit_outlives_requirements,
31    exported_private_dependencies,
32    forbidden_lint_groups,
33    function_item_references,
34    future_incompatible,
35    ill_formed_attribute_input,
36    improper_ctypes_definitions,
37    improper_ctypes,
38    incomplete_features,
39    incomplete_include,
40    ineffective_unstable_trait_impl,
41    inline_no_sanitize,
42    invalid_atomic_ordering,
43    invalid_doc_attributes,
44    invalid_type_param_default,
45    invalid_value,
46    irrefutable_let_patterns,
47    keyword_idents,
48    large_assignments,
49    late_bound_lifetime_arguments,
50    legacy_derive_helpers,
51    macro_expanded_macro_exports_accessed_by_absolute_paths,
52    meta_variable_misuse,
53    missing_abi,
54    missing_copy_implementations,
55    missing_debug_implementations,
56    missing_docs,
57    mixed_script_confusables,
58    mutable_transmutes,
59    named_arguments_used_positionally,
60    named_asm_labels,
61    no_mangle_const_items,
62    no_mangle_generic_items,
63    non_ascii_idents,
64    non_camel_case_types,
65    non_fmt_panics,
66    non_shorthand_field_patterns,
67    non_snake_case,
68    non_upper_case_globals,
69    nonstandard_style,
70    noop_method_call,
71    overflowing_literals,
72    overlapping_range_endpoints,
73    path_statements,
74    patterns_in_fns_without_body,
75    proc_macro_derive_resolution_fallback,
76    pub_use_of_private_extern_crate,
77    redundant_semicolons,
78    repr_transparent_external_private_fields,
79    rust_2021_incompatible_closure_captures,
80    rust_2021_incompatible_or_patterns,
81    rust_2021_prefixes_incompatible_syntax,
82    rust_2021_prelude_collisions,
83    semicolon_in_expressions_from_macros,
84    soft_unstable,
85    stable_features,
86    text_direction_codepoint_in_comment,
87    text_direction_codepoint_in_literal,
88    trivial_bounds,
89    trivial_casts,
90    trivial_numeric_casts,
91    type_alias_bounds,
92    tyvar_behind_raw_pointer,
93    uncommon_codepoints,
94    unconditional_panic,
95    unconditional_recursion,
96    unexpected_cfgs,
97    uninhabited_static,
98    unknown_crate_types,
99    unnameable_test_items,
100    unreachable_code,
101    unreachable_patterns,
102    unreachable_pub,
103    unsafe_op_in_unsafe_fn,
104    unstable_features,
105    unstable_name_collisions,
106    unused_allocation,
107    unused_assignments,
108    unused_attributes,
109    unused_braces,
110    unused_comparisons,
111    unused_crate_dependencies,
112    unused_doc_comments,
113    unused_extern_crates,
114    unused_features,
115    unused_import_braces,
116    unused_imports,
117    unused_labels,
118    unused_lifetimes,
119    unused_macro_rules,
120    unused_macros,
121    unused_must_use,
122    unused_mut,
123    unused_parens,
124    unused_qualifications,
125    unused_unsafe,
126    unused_variables,
127    useless_deprecated,
128    while_true
129)]
130#![warn(
131    clippy::all,
132    clippy::await_holding_lock,
133    clippy::char_lit_as_u8,
134    clippy::checked_conversions,
135    clippy::cognitive_complexity,
136    clippy::dbg_macro,
137    clippy::debug_assert_with_mut_call,
138    clippy::disallowed_script_idents,
139    clippy::doc_link_with_quotes,
140    clippy::doc_markdown,
141    clippy::empty_enum,
142    clippy::empty_line_after_outer_attr,
143    clippy::empty_structs_with_brackets,
144    clippy::enum_glob_use,
145    clippy::equatable_if_let,
146    clippy::exit,
147    clippy::expl_impl_clone_on_copy,
148    clippy::explicit_deref_methods,
149    clippy::explicit_into_iter_loop,
150    clippy::fallible_impl_from,
151    clippy::filter_map_next,
152    clippy::flat_map_option,
153    clippy::float_cmp_const,
154    clippy::float_cmp,
155    clippy::float_equality_without_abs,
156    clippy::fn_params_excessive_bools,
157    clippy::fn_to_numeric_cast_any,
158    clippy::from_iter_instead_of_collect,
159    clippy::if_let_mutex,
160    clippy::implicit_clone,
161    clippy::imprecise_flops,
162    clippy::index_refutable_slice,
163    clippy::inefficient_to_string,
164    clippy::invalid_upcast_comparisons,
165    clippy::iter_not_returning_iterator,
166    clippy::large_digit_groups,
167    clippy::large_stack_arrays,
168    clippy::large_types_passed_by_value,
169    clippy::let_unit_value,
170    clippy::linkedlist,
171    clippy::lossy_float_literal,
172    clippy::macro_use_imports,
173    clippy::manual_ok_or,
174    clippy::map_err_ignore,
175    clippy::map_flatten,
176    clippy::map_unwrap_or,
177    clippy::match_same_arms,
178    clippy::match_wild_err_arm,
179    clippy::match_wildcard_for_single_variants,
180    clippy::mem_forget,
181    clippy::missing_const_for_fn,
182    clippy::missing_enforced_import_renames,
183    clippy::missing_errors_doc,
184    clippy::missing_panics_doc,
185    clippy::mut_mut,
186    clippy::mutex_integer,
187    clippy::needless_borrow,
188    clippy::needless_continue,
189    clippy::needless_for_each,
190    clippy::needless_pass_by_value,
191    clippy::negative_feature_names,
192    clippy::nonstandard_macro_braces,
193    clippy::nursery,
194    clippy::option_if_let_else,
195    clippy::option_option,
196    clippy::path_buf_push_overwrite,
197    clippy::pedantic,
198    clippy::print_stderr,
199    clippy::print_stdout,
200    clippy::ptr_as_ptr,
201    clippy::rc_mutex,
202    clippy::ref_option_ref,
203    clippy::rest_pat_in_fully_bound_structs,
204    clippy::same_functions_in_if_condition,
205    clippy::semicolon_if_nothing_returned,
206    clippy::shadow_unrelated,
207    clippy::similar_names,
208    clippy::single_match_else,
209    clippy::string_add_assign,
210    clippy::string_add,
211    clippy::string_lit_as_bytes,
212    clippy::suspicious_operation_groupings,
213    clippy::todo,
214    clippy::trailing_empty_array,
215    clippy::trait_duplication_in_bounds,
216    clippy::trivially_copy_pass_by_ref,
217    clippy::unimplemented,
218    clippy::unnecessary_wraps,
219    clippy::unnested_or_patterns,
220    clippy::unseparated_literal_suffix,
221    clippy::unused_self,
222    clippy::use_debug,
223    clippy::use_self,
224    clippy::used_underscore_binding,
225    clippy::useless_let_if_seq,
226    clippy::useless_transmute,
227    clippy::verbose_file_reads,
228    clippy::wildcard_dependencies,
229    clippy::wildcard_imports,
230    clippy::zero_sized_map_values
231)]
232
233//! # sable-canvas
234//!
235//! `sable-canvas` is a library for creating and manipulating JSON objects representing a canvas.
236//!
237//! Specification source: <https://jsoncanvas.org/>
238//!
239//! ## Example
240//!
241//! ```
242//! use sable_canvas::Canvas;
243//! let s: String = "{\"nodes\":[{\"id\":\"id7\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"background\":\"path/to/image.png\",\"type\":\"group\"},{\"id\":\"id5\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"color\":\"#ff0000\",\"label\":\"Label\",\"type\":\"group\"},{\"id\":\"id2\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"color\":\"1\",\"file\":\"dir/to/path/file.png\",\"type\":\"file\"},{\"id\":\"id4\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"color\":\"1\",\"url\":\"https://www.google.com\",\"type\":\"link\"},{\"id\":\"id6\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"type\":\"group\"},{\"id\":\"id3\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"color\":\"1\",\"file\":\"dir/to/path/file.png\",\"subpath\":\"#here\",\"type\":\"file\"},{\"id\":\"id8\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"background\":\"path/to/image.png\",\"backgroundStyle\":\"cover\",\"type\":\"group\"},{\"id\":\"id\",\"x\":0,\"y\":0,\"width\":100,\"height\":100,\"color\":\"1\",\"text\":\"Test\",\"type\":\"text\"}],\"edges\":[{\"id\":\"edge2\",\"fromNode\":\"node3\",\"toNode\":\"node4\",\"color\":\"5\",\"label\":\"edge label\",\"toSide\":\"left\",\"toEnd\":\"arrow\"},{\"id\":\"edge1\",\"fromNode\":\"node1\",\"toNode\":\"node2\",\"toSide\":\"left\",\"toEnd\":\"arrow\"}]}".to_string();
244//! let canvas: Canvas = s.parse().unwrap();
245//!
246//! let _s = canvas.to_string();
247//! ```
248//!
249//! ## Complete example
250//!
251//! ```rust
252//! use std::path::PathBuf;
253//!
254//! use sable_canvas::{
255//!     Canvas,
256//!     Color, HexColor, PresetColor,
257//!     Edge, End, Side,
258//!     Background, BackgroundStyle, FileNode, GroupNode, LinkNode, Node, TextNode,
259//! };
260//! use url::Url;
261//!
262//! // Color
263//! let color1 = Color::Preset(PresetColor::Red);
264//! let color2 = Color::Color(HexColor::parse("#ff0000").unwrap());
265//!
266//! let serialized_color1 = serde_json::to_string(&color1).unwrap();
267//! let serialized_color2 = serde_json::to_string(&color2).unwrap();
268//!
269//! println!("serialized1 = {}", serialized_color1);
270//! println!("serialized2 = {}", serialized_color2);
271//!
272//! // Text Node
273//! let node1: Node = Node::Text(TextNode::new(
274//!     "id".parse().unwrap(),
275//!     0,
276//!     0,
277//!     100,
278//!     100,
279//!     Some(Color::Preset(PresetColor::Red)),
280//!     "This is a test".to_string(),
281//! ));
282//!
283//! // File Node
284//! let node2: Node = Node::File(FileNode::new(
285//!     "id2".parse().unwrap(),
286//!     0,
287//!     0,
288//!     100,
289//!     100,
290//!     Some(Color::Preset(PresetColor::Red)),
291//!     PathBuf::from("dir/to/path/file.png"),
292//!     None,
293//! ));
294//! let node3: Node = Node::File(FileNode::new(
295//!     "id3".parse().unwrap(),
296//!     0,
297//!     0,
298//!     100,
299//!     100,
300//!     Some(color1),
301//!     PathBuf::from("dir/to/path/file.png"),
302//!     Some("#here".parse().unwrap()),
303//! ));
304//!
305//! // Link Node
306//! let node4: Node = Node::Link(LinkNode::new(
307//!     "id4".parse().unwrap(),
308//!     0,
309//!     0,
310//!     100,
311//!     100,
312//!     Some(Color::Preset(PresetColor::Red)),
313//!     Url::parse("https://julienduroure.com").unwrap(),
314//! ));
315//!
316//! // Group Node
317//! let node5: Node = Node::Group(GroupNode::new(
318//!     "id5".parse().unwrap(),
319//!     0,
320//!     0,
321//!     100,
322//!     100,
323//!     Some(color2),
324//!     Some("Label".to_string()),
325//!     None,
326//! ));
327//! let node6: Node = Node::Group(GroupNode::new(
328//!     "id6".parse().unwrap(),
329//!     0,
330//!     0,
331//!     100,
332//!     100,
333//!     None,
334//!     None,
335//!     None,
336//! ));
337//! let node7: Node = Node::Group(GroupNode::new(
338//!     "id7".parse().unwrap(),
339//!     0,
340//!     0,
341//!     100,
342//!     100,
343//!     None,
344//!     None,
345//!     Some(Background::new(PathBuf::from("path/to/image.png"), None)),
346//! ));
347//! let node8: Node = Node::Group(GroupNode::new(
348//!     "id8".parse().unwrap(),
349//!     0,
350//!     0,
351//!     100,
352//!     100,
353//!     None,
354//!     None,
355//!     Some(Background::new(
356//!         PathBuf::from("path/to/image.png"),
357//!         Some(BackgroundStyle::Cover),
358//!     )),
359//! ));
360//!
361//! let serialized_node1: String = serde_json::to_string(&node1).unwrap();
362//! let serialized_node2 = serde_json::to_string(&node2).unwrap();
363//! let serialized_node3 = serde_json::to_string(&node3).unwrap();
364//! let serialized_node4 = serde_json::to_string(&node4).unwrap();
365//! let serialized_node5 = serde_json::to_string(&node5).unwrap();
366//! let serialized_node6 = serde_json::to_string(&node6).unwrap();
367//! let serialized_node7 = serde_json::to_string(&node7).unwrap();
368//! let serialized_node8 = serde_json::to_string(&node8).unwrap();
369//!
370//! println!("serialized node 1= {}", serialized_node1);
371//! println!("serialized node 2= {}", serialized_node2);
372//! println!("serialized node 3= {}", serialized_node3);
373//! println!("serialized node 4= {}", serialized_node4);
374//! println!("serialized node 5= {}", serialized_node5);
375//! println!("serialized node 6= {}", serialized_node6);
376//! println!("serialized node 7= {}", serialized_node7);
377//! println!("serialized node 8= {}", serialized_node8);
378//!
379//! // Edge
380//! let edge1 = Edge::new(
381//!     "edge1".parse().unwrap(),
382//!     "id".parse().unwrap(),
383//!     None,
384//!     None,
385//!     "id2".parse().unwrap(),
386//!     Some(Side::Left),
387//!     Some(End::Arrow),
388//!     None,
389//!     None,
390//! );
391//! let edge2 = Edge::new(
392//!     "edge2".parse().unwrap(),
393//!     "id3".parse().unwrap(),
394//!     None,
395//!     None,
396//!     "id4".parse().unwrap(),
397//!     Some(Side::Left),
398//!     Some(End::Arrow),
399//!     Some(Color::Preset(PresetColor::Cyan)),
400//!     Some("edge label".to_string()),
401//! );
402//!
403//! let serialized_edge1 = serde_json::to_string(&edge1).unwrap();
404//! let serialized_edge2 = serde_json::to_string(&edge2).unwrap();
405//!
406//! println!("serialized edge 1= {}", serialized_edge1);
407//! println!("serialized edge 2= {}", serialized_edge2);
408//!
409//! // JSON Canvas
410//! let mut canvas = Canvas::default();
411//!
412//! let empty_canvas = canvas.to_string();
413//! println!("empty canvas = {}", empty_canvas);
414//! canvas = empty_canvas.parse().unwrap();
415//!
416//! canvas.add_node(node1).unwrap();
417//! canvas.add_node(node2).unwrap();
418//! canvas.add_node(node3).unwrap();
419//! canvas.add_node(node4).unwrap();
420//! canvas.add_node(node5).unwrap();
421//! canvas.add_node(node6).unwrap();
422//! canvas.add_node(node7).unwrap();
423//! canvas.add_node(node8).unwrap();
424//!
425//! canvas.add_edge(edge1).unwrap();
426//! canvas.add_edge(edge2).unwrap();
427//!
428//! let serialized_canvas = canvas.to_string();
429//!
430//! println!("serialized canvas = {}", serialized_canvas);
431//!
432//! let jsoncanvas_deserialized: Canvas = serialized_canvas.parse().unwrap();
433//! println!("deserialized canvas = {:?}", jsoncanvas_deserialized);
434//! ```
435//!
436//! ## Available structs
437//!
438//! ```
439//! use sable_canvas::{
440//!     Canvas,
441//!     Color, HexColor, PresetColor,
442//!     Edge, End, Side,
443//!     Background, BackgroundStyle, FileNode, GroupNode, LinkNode, Node, TextNode,
444//! };
445//! ```
446
447mod canvas;
448mod color;
449mod edge;
450mod id;
451mod node;
452
453pub use crate::{
454    canvas::{Canvas, CanvasError},
455    color::{Color, HexColor, PresetColor},
456    edge::{Edge, End, Side, Terminus},
457    id::{EdgeId, EmptyId, NodeId},
458    node::{
459        Background, BackgroundStyle, FileNode, GenericNode, GenericNodeInfo, GroupNode, LinkNode,
460        Node, TextNode,
461    },
462};
463
464/// Type alias for the pixel coordinate unit.
465pub type PixelCoordinate = i64;
466/// Type alias for the pixel dimension unit.
467pub type PixelDimension = u64;
468
469#[cfg(test)]
470mod test {
471    use hex_color::HexColor;
472
473    #[allow(clippy::too_many_lines, reason = "this is a test... :/")]
474    #[allow(clippy::print_stdout)]
475    #[test]
476    fn test() {
477        use std::path::PathBuf;
478
479        use url::Url;
480
481        use super::{
482            canvas::Canvas,
483            color::{Color, PresetColor},
484            edge::{Edge, End, Side},
485            node::{Background, BackgroundStyle, FileNode, GroupNode, LinkNode, Node, TextNode},
486        };
487
488        // Color
489        let color1 = Color::Preset(PresetColor::Red);
490        let color2 = Color::Color(HexColor::parse("#ff0000").unwrap());
491
492        // Text Node
493        let node1: Node = TextNode::new(
494            "id".parse().unwrap(),
495            0,
496            0,
497            100,
498            100,
499            Some(Color::Preset(PresetColor::Red)),
500            "This is a test".to_string(),
501        )
502        .into();
503
504        // File Node
505        let node2: Node = FileNode::new(
506            "id2".parse().unwrap(),
507            0,
508            0,
509            100,
510            100,
511            Some(Color::Preset(PresetColor::Red)),
512            PathBuf::from("dir/to/path/file.png"),
513            None,
514        )
515        .into();
516        let node3: Node = FileNode::new(
517            "id3".parse().unwrap(),
518            0,
519            0,
520            100,
521            100,
522            Some(color1),
523            PathBuf::from("dir/to/path/file.png"),
524            Some("#here".to_string()),
525        )
526        .into();
527
528        // Link Node
529        let node4: Node = LinkNode::new(
530            "id4".parse().unwrap(),
531            0,
532            0,
533            100,
534            100,
535            Some(Color::Preset(PresetColor::Red)),
536            Url::parse("https://julienduroure.com").unwrap(),
537        )
538        .into();
539
540        // Group Node
541        let node5: Node = GroupNode::new(
542            "id5".parse().unwrap(),
543            0,
544            0,
545            100,
546            100,
547            Some(color2),
548            Some("Label".to_string()),
549            None,
550        )
551        .into();
552        let node6: Node =
553            GroupNode::new("id6".parse().unwrap(), 0, 0, 100, 100, None, None, None).into();
554        let node7: Node = GroupNode::new(
555            "id7".parse().unwrap(),
556            0,
557            0,
558            100,
559            100,
560            None,
561            None,
562            Some(Background::new(PathBuf::from("path/to/image.png"), None)),
563        )
564        .into();
565        let node8: Node = GroupNode::new(
566            "id8".parse().unwrap(),
567            0,
568            0,
569            100,
570            100,
571            None,
572            None,
573            Some(Background::new(
574                PathBuf::from("path/to/image.png"),
575                Some(BackgroundStyle::Cover),
576            )),
577        )
578        .into();
579
580        // Edge
581
582        let edge1 = Edge::new(
583            "edge1".parse().unwrap(),
584            "id".parse().unwrap(),
585            None,
586            None,
587            "id2".parse().unwrap(),
588            Some(Side::Left),
589            Some(End::Arrow),
590            None,
591            None,
592        );
593        let edge2 = Edge::new(
594            "edge2".parse().unwrap(),
595            "id3".parse().unwrap(),
596            None,
597            None,
598            "id4".parse().unwrap(),
599            Some(Side::Left),
600            Some(End::Arrow),
601            Some(Color::Preset(PresetColor::Cyan)),
602            Some("edge label".to_string()),
603        );
604
605        // JSON Canvas
606        let mut canvas = Canvas::default();
607        canvas.add_node(node1).unwrap();
608        canvas.add_node(node2).unwrap();
609        canvas.add_node(node3).unwrap();
610        canvas.add_node(node4).unwrap();
611        canvas.add_node(node5).unwrap();
612        canvas.add_node(node6).unwrap();
613        canvas.add_node(node7).unwrap();
614        canvas.add_node(node8).unwrap();
615
616        canvas.add_edge(edge1).unwrap();
617        canvas.add_edge(edge2).unwrap();
618
619        let serialized_canvas = canvas.to_string();
620
621        println!("serialized canvas = {serialized_canvas}");
622
623        ///////////////////////////// Deserialization /////////////////////////////
624
625        // let deserialized_node1: Node = serde_json::from_str(&serialized_node1).unwrap();
626        // println!("deserialized node 1= {:?}", deserialized_node1);
627
628        // let deseralied_edge1: Edge = serde_json::from_str(&serialized_edge1).unwrap();
629        // println!("deserialized edge 1= {:?}", deseralied_edge1);
630
631        let _jsoncanvas_deserialized: Canvas = serialized_canvas.parse().unwrap();
632    }
633}
634