1 module io;
2 
3 import std.range;
4 import std.stdio;
5 import std.typecons;
6 
7 Input read(string name, Flag!"offset" offset = No.offset)
8 {
9     return read(name, File(name), offset);
10 }
11 
12 Input read(string name, File file, Flag!"offset" offset = No.offset)
13 {
14     import std.algorithm : joiner;
15     import std.array : array;
16 
17     auto text = cast(char[]) file.byChunk(4096).joiner.array;
18 
19     return Input(name, text, offset);
20 }
21 
22 struct Input
23 {
24     private string name;
25 
26     private const(char)[] text;
27 
28     private size_t index_;
29 
30     private size_t begin;
31 
32     private size_t line = 1;
33 
34     private size_t col = 1; // offset, for offset-based positions
35 
36     private Flag!"offset" offset;
37 
38     this(string name, const(char)[] text, Flag!"offset" offset = No.offset) @nogc nothrow
39     {
40         this.name = name;
41         this.text = text;
42         this.offset = offset;
43         if (offset)
44             line = col = 0;
45     }
46 
47     void popFront()
48     in (!empty)
49     {
50         import std.utf : stride;
51 
52         const lineBreak = front == '\n';
53 
54         if (lineBreak && !offset)
55         {
56             ++line;
57             col = 0;
58         }
59         index_ += text[index_ .. $].stride;
60         ++col;
61         if (lineBreak)
62             begin = index_;
63     }
64 
65     dchar front() const
66     {
67         return empty ? 0 : text[index_ .. $].front;
68     }
69 
70     bool empty() const @nogc nothrow
71     {
72         return text[index_ .. $].empty;
73     }
74 
75     Position position() const pure @safe
76     {
77         import std.algorithm : find;
78 
79         const end = text.length - text[begin .. $].find('\n').length;
80 
81         return Position(name, line, col, text[begin .. end]);
82     }
83 
84     const(char)[] sliceFrom(size_t begin) const @nogc nothrow
85     in (begin <= index)
86     {
87         return text[begin .. index_];
88     }
89 
90     size_t index() const @nogc nothrow
91     {
92         return index_;
93     }
94 }
95 
96 const UndefPos = Position();
97 
98 struct Position
99 {
100     private string name;
101 
102     private size_t line;
103 
104     private size_t col; // offset, if line is 0
105 
106     private const(char)[] text;
107 
108     string toString() const @safe
109     {
110         import std.array : appender;
111 
112         auto writer = appender!string;
113 
114         toString(writer);
115         return writer[];
116     }
117 
118     void toString(W)(ref W writer) const
119     if (isOutputRange!(W, char))
120     {
121         import std.algorithm : map;
122         import std.format : format;
123         import std.utf : count;
124 
125         if (this == UndefPos)
126         {
127             writer.put("unknown position");
128             return;
129         }
130 
131         if (line == 0) // offset position
132         {
133             const link = format!"%s@%s"(name, col);
134 
135             writer.put(link);
136             return;
137         }
138 
139         const link = format!"%s:%s:%s"(name, line, col);
140 
141         writer.put(link);
142         writer.put(' ');
143         writer.put(text);
144         writer.put('\n');
145         writer.put(' ');
146         writer.put(' '.repeat(link.count));
147         writer.put(text.take(col - 1).map!(c => (c == '\t') ? c : ' '));
148         writer.put('^');
149     }
150 }
151 
152 @("convert position to string")
153 unittest
154 {
155     import std.string : outdent, strip;
156 
157     const position = Position("äöü.txt", 42, 2, "äöü");
158     const expected = `
159         äöü.txt:42:2 äöü
160                       ^
161         `.outdent.strip;
162 
163     assert(position.toString == expected);
164 }
165 
166 @("convert offset position to string")
167 unittest
168 {
169     import std.string : outdent, strip;
170 
171     const position = Position("äöü.txt", 0, 42, "äöü");
172     const expected = `
173         äöü.txt@42
174         `.outdent.strip;
175 
176     assert(position.toString == expected);
177 }
178 
179 @("convert unknown position to string")
180 unittest
181 {
182     import std.string : outdent, strip;
183 
184     const expected = `
185         unknown position
186         `.outdent.strip;
187 
188     assert(UndefPos.toString == expected);
189 }