lib/zen/src/diagram/ego/model.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 pub const max_source_nodes_per_band: usize = 12;
  4 pub const max_slots_per_band: usize = 6;
  5 pub const margin_slots_per_band: u8 = 3;
  6 pub const wide_slots_per_band: u8 = 6;
  7 pub const label_bytes_max: usize = 40;
  8 pub const context_bytes_max: usize = 48;
  9 pub const badge_bytes_max: usize = 48;
 10 pub const title_bytes_max: usize = 192;
 11 pub const href_bytes_max: usize = 512;
 12 
 13 /// One declaration in a call map. An empty href renders non-interactive text.
 14 pub const Node = struct {
 15     label: []const u8,
 16     context: []const u8 = "",
 17     badge: []const u8 = "",
 18     title: []const u8 = "",
 19     href: []const u8,
 20 };
 21 
 22 /// One call direction with exact total count and a bounded ordered sample.
 23 pub const Band = struct {
 24     nodes: []const Node = &.{},
 25     total: u32 = 0,
 26     more_href: []const u8 = "",
 27 };
 28 
 29 /// A declaration and its direct callers and callees.
 30 pub const Network = struct {
 31     subject: Node,
 32     callers: Band = .{},
 33     callees: Band = .{},
 34     recursive: bool = false,
 35 };
 36 
 37 /// Selects geometry for a documentation margin or the wider reading surface.
 38 pub const Surface = enum {
 39     margin,
 40     wide,
 41 };
 42 
 43 /// Returns the bounded visible slot count for one call-map band.
 44 pub fn slotsPerBand(surface: Surface) u8 {
 45     return switch (surface) {
 46         .margin => margin_slots_per_band,
 47         .wide => wide_slots_per_band,
 48     };
 49 }
 50 
 51 pub const Arrangement = enum {
 52     horizontal,
 53     vertical,
 54 };
 55 
 56 pub const Geometry = struct {
 57     arrangement: Arrangement,
 58     width: u16,
 59     height: u16,
 60     slots_per_band: u8,
 61     outer_inset: f32,
 62     node_width: f32,
 63     subject_width: f32,
 64     node_height: f32,
 65     subject_height: f32,
 66     heading_size: f32,
 67     empty_size: f32,
 68     label_size: f32,
 69     context_size: f32,
 70     badge_size: f32,
 71     subject_label_size: f32,
 72 };
 73 
 74 pub const Error = error{
 75     EmptyNodeLabel,
 76     InvalidBandTotal,
 77     InvalidText,
 78     MissingOverflowLink,
 79     NodeBadgeTooLong,
 80     NodeContextTooLong,
 81     NodeLabelTooLong,
 82     NodeLinkTooLong,
 83     NodeTitleTooLong,
 84     TooManySourceNodes,
 85     UnsafeNodeLink,
 86 };
 87 
 88 pub fn validate(network: Network) Error!void {
 89     try validateNode(network.subject);
 90     try validateBand(network.callers);
 91     try validateBand(network.callees);
 92 }
 93 
 94 pub fn validateNode(node: Node) Error!void {
 95     if (node.label.len == 0) return error.EmptyNodeLabel;
 96     if (node.label.len > label_bytes_max) return error.NodeLabelTooLong;
 97     if (node.context.len > context_bytes_max) return error.NodeContextTooLong;
 98     if (node.badge.len > badge_bytes_max) return error.NodeBadgeTooLong;
 99     if (node.title.len > title_bytes_max) return error.NodeTitleTooLong;
100     if (node.href.len > href_bytes_max) return error.NodeLinkTooLong;
101     if (!validText(node.label) or !validText(node.context) or
102         !validText(node.badge) or
103         !validText(node.title) or !validText(node.href)) return error.InvalidText;
104     if (node.href.len > 0 and !safeHref(node.href)) return error.UnsafeNodeLink;
105 }
106 
107 pub fn validateBand(band: Band) Error!void {
108     if (band.nodes.len > max_source_nodes_per_band) {
109         return error.TooManySourceNodes;
110     }
111     if (band.total < band.nodes.len) return error.InvalidBandTotal;
112     std.debug.assert(band.nodes.len <= max_source_nodes_per_band);
113     for (band.nodes) |node| try validateNode(node);
114     if (band.more_href.len > href_bytes_max) return error.NodeLinkTooLong;
115     if (!validText(band.more_href)) return error.InvalidText;
116     if (band.more_href.len > 0 and !safeHref(band.more_href)) {
117         return error.UnsafeNodeLink;
118     }
119 }
120 
121 fn validText(value: []const u8) bool {
122     if (!std.unicode.utf8ValidateSlice(value)) return false;
123     for (value) |byte| {
124         if (byte < ' ' and byte != '\t') return false;
125         if (byte == 0x7f) return false;
126     }
127     return true;
128 }
129 
130 pub fn safeHref(value: []const u8) bool {
131     std.debug.assert(value.len > 0);
132     if (switch (value[0]) {
133         '#', '/', '?', '.' => true,
134         else => false,
135     }) return true;
136     const authority_start = if (std.mem.startsWith(u8, value, "https://"))
137         "https://".len
138     else if (std.mem.startsWith(u8, value, "http://"))
139         "http://".len
140     else
141         return false;
142     const authority_end = std.mem.indexOfScalarPos(
143         u8,
144         value,
145         authority_start,
146         '/',
147     ) orelse value.len;
148     if (authority_end == authority_start) return false;
149     for (value) |byte| switch (byte) {
150         ' ', '\t', '\\', '"', '\'', '<', '>' => return false,
151         else => {},
152     };
153     return true;
154 }