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 }