lib/zen/src/quiz/parse.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const model = @import("model.zig");
3 const scan = @import("scan.zig");
4 const storage_mod = @import("storage.zig");
5
6 pub fn parse(storage: *storage_mod.Storage, source: []const u8) model.Error!model.Quiz {
7 var regions = try storage.acquire(source);
8 var iterator = scan.Iterator.init(source);
9 var question_index: usize = 0;
10 var option_index: usize = 0;
11 var byte_index: usize = 0;
12 var current: ?Current = null;
13
14 while (iterator.next()) |token| switch (token) {
15 .prompt => |text| {
16 if (current) |active| active.finish(®ions, option_index);
17 std.debug.assert(question_index < regions.questions.len);
18 regions.questions[question_index] = .{
19 .prompt = text,
20 .options = &.{},
21 .explanation = "",
22 };
23 current = .{
24 .question = question_index,
25 .option_start = option_index,
26 .active = .{ .prompt = question_index },
27 };
28 question_index += 1;
29 },
30 .option => |option| {
31 std.debug.assert(current != null);
32 var active = ¤t.?;
33 std.debug.assert(option_index < regions.options.len);
34 regions.options[option_index] = .{
35 .text = option.text,
36 .correct = option.correct,
37 };
38 active.active = .{ .option = option_index };
39 active.joined_start = null;
40 option_index += 1;
41 },
42 .explanation => |text| {
43 std.debug.assert(current != null);
44 var active = ¤t.?;
45 switch (active.active) {
46 .explanation => active.join(®ions, &byte_index, text),
47 else => {
48 regions.questions[active.question].explanation = text;
49 active.active = .{ .explanation = active.question };
50 active.joined_start = null;
51 },
52 }
53 },
54 .continuation => |text| {
55 std.debug.assert(current != null);
56 var active = ¤t.?;
57 active.join(®ions, &byte_index, text);
58 },
59 };
60 std.debug.assert(current != null);
61 current.?.finish(®ions, option_index);
62 std.debug.assert(question_index == regions.questions.len);
63 std.debug.assert(option_index == regions.options.len);
64 std.debug.assert(byte_index == regions.joined_text.len);
65 return .{ .questions = regions.questions };
66 }
67
68 const Active = union(enum) {
69 prompt: usize,
70 option: usize,
71 explanation: usize,
72
73 fn target(self: Active, regions: *storage_mod.Regions) *[]const u8 {
74 return switch (self) {
75 .prompt => |index| ®ions.questions[index].prompt,
76 .option => |index| ®ions.options[index].text,
77 .explanation => |index| ®ions.questions[index].explanation,
78 };
79 }
80 };
81
82 const Current = struct {
83 question: usize,
84 option_start: usize,
85 active: Active,
86 joined_start: ?usize = null,
87
88 fn finish(self: Current, regions: *storage_mod.Regions, option_index: usize) void {
89 std.debug.assert(self.question < regions.questions.len);
90 std.debug.assert(self.option_start <= option_index);
91 std.debug.assert(option_index <= regions.options.len);
92 regions.questions[self.question].options = regions.options[self.option_start..option_index];
93 }
94
95 fn join(
96 self: *Current,
97 regions: *storage_mod.Regions,
98 byte_index: *usize,
99 text: []const u8,
100 ) void {
101 const target = self.active.target(regions);
102 const start = self.joined_start orelse start: {
103 const value = byte_index.*;
104 copyText(regions.joined_text, byte_index, target.*);
105 self.joined_start = value;
106 break :start value;
107 };
108 copyText(regions.joined_text, byte_index, " ");
109 copyText(regions.joined_text, byte_index, text);
110 target.* = regions.joined_text[start..byte_index.*];
111 }
112 };
113
114 fn copyText(destination: []u8, index: *usize, bytes: []const u8) void {
115 std.debug.assert(index.* <= destination.len);
116 std.debug.assert(bytes.len <= destination.len - index.*);
117 @memcpy(destination[index.*..][0..bytes.len], bytes);
118 index.* += bytes.len;
119 }