diff --git a/io/LineWriter.cs b/io/LineWriter.cs
new file mode 100644
index 0000000..ec8f843
--- /dev/null
+++ b/io/LineWriter.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace ln.types.io
+{
+ public delegate void LineReceivedDelegate(object sender, string line);
+
+ public class LineWriter : TextWriter
+ {
+ public event LineReceivedDelegate LineReceived;
+ public override Encoding Encoding => Encoding.UTF8;
+
+ private void FireLineReceived(string line) => LineReceived?.Invoke(this, line);
+
+ char[] lineBuffer = new char[8192];
+ int cursor = 0;
+
+ String CurrentLineBuffer => new string(lineBuffer, 0, cursor);
+
+ public LineWriter()
+ {
+ }
+
+ public override void Write(char value)
+ {
+ lock (this)
+ {
+ if (value == '\n')
+ {
+ FireLineReceived(CurrentLineBuffer);
+ cursor = 0;
+ } else if (cursor < lineBuffer.Length)
+ {
+ lineBuffer[cursor++] = value;
+ }
+ }
+ }
+
+ public override void WriteLine() => Write('\n');
+ public override void WriteLine(string value)
+ {
+ lock (this)
+ {
+ if (cursor > 0)
+ {
+ value = CurrentLineBuffer + value;
+ cursor = 0;
+ }
+ FireLineReceived(value);
+ }
+ }
+
+ }
+}
diff --git a/ln.types.csproj b/ln.types.csproj
index d1fe40f..bb3b22f 100644
--- a/ln.types.csproj
+++ b/ln.types.csproj
@@ -137,6 +137,8 @@
+
+
@@ -160,6 +162,7 @@
+
diff --git a/text/TokenReader.cs b/text/TokenReader.cs
new file mode 100644
index 0000000..ee7056b
--- /dev/null
+++ b/text/TokenReader.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Linq;
+namespace ln.types.text
+{
+ public class TokenReader
+ {
+ public TextReader Reader { get; }
+
+ public TokenReader(Stream stream)
+ : this(new UnbufferedStreamReader(stream)) { }
+ public TokenReader(TextReader textReader)
+ {
+ Reader = textReader;
+ }
+
+ public String Read(Func criteria)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ int ch;
+ while (((ch = Reader.Peek()) != -1) && criteria((char)ch))
+ stringBuilder.Append((char)Reader.Read());
+
+ return stringBuilder.ToString();
+ }
+
+ public bool EndOfStream => Reader.Peek() == -1;
+
+ public String Read(char validChar) => Read((ch) => ch == validChar);
+ public String Read(char[] validChars) => Read((ch) => validChars.Contains(ch));
+
+ public String ReadWhiteSpace() => Read((ch) => char.IsWhiteSpace(ch));
+ public String ReadToken() => Read((ch) => char.IsLetterOrDigit(ch) || (ch == '_'));
+
+ public String ReadValue() => Reader.Peek() == '"' ? ReadQuotedString() : ReadToken();
+
+ public String ReadQuotedString()
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ int nextCh = Reader.Peek();
+ if (nextCh != -1)
+ {
+ if (nextCh != '"')
+ throw new FormatException("expected double quotes");
+ Reader.Read();
+
+ while (((nextCh = Reader.Read()) != -1) && (nextCh != '"'))
+ {
+ if (nextCh == '\\')
+ nextCh = Reader.Read();
+ stringBuilder.Append((char)nextCh);
+ }
+ }
+
+ return stringBuilder.ToString();
+ }
+
+ public String ReadPath()
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ string token;
+
+ if (Reader.Peek() == '/')
+ stringBuilder.Append((char)Reader.Read());
+
+ while ((token = ReadToken()).Length > 0)
+ {
+ stringBuilder.Append(token);
+
+ if (Reader.Peek() == '/')
+ stringBuilder.Append((char)Reader.Read());
+ else
+ break;
+ }
+
+ return stringBuilder.ToString();
+ }
+
+ class UnbufferedStreamReader : TextReader
+ {
+ public Stream BaseStream { get; }
+
+ public UnbufferedStreamReader(Stream stream)
+ {
+ BaseStream = stream;
+ }
+ public override int Read() => BaseStream.ReadByte();
+ }
+ }
+}