Files
hank/hank/HankBuffer.hx
Nat Quayle Nelson a7bd12074c
Some checks failed
/ test (push) Failing after 47s
hxnodejs allow getContent
2025-10-29 15:02:14 -05:00

436 lines
12 KiB
Haxe

package hank;
using StringTools;
import haxe.ds.Option;
typedef PreloadedFiles = Map<String, String>;
/**
A position in a HankBuffer, used for debugging.
**/
class Position {
public var file:String;
public var line:Int;
public var column:Int;
public function new(file:String, line:Int, column:Int) {
this.file = file;
this.line = line;
this.column = column;
}
public function equals(other:Position) {
return file == other.file && line == other.line && column == other.column;
}
}
/**
Reference to a slice of the buffer, which expires when the buffer changes its state.
**/
@:allow(hank.HankBuffer)
class BufferSlice {
public var start(default, null):Int;
public var length(default, null):Int;
var anchorPosition:Position;
var buffer:HankBuffer;
private function new(start:Int, length:Int, buffer:HankBuffer) {
this.start = start;
this.length = length;
this.anchorPosition = buffer.position();
this.buffer = buffer;
}
public function checkValue():String {
if (!buffer.position().equals(anchorPosition)) {
throw 'Tried to access an expired BufferSlice.';
}
return buffer.peekAhead(start, length);
}
}
typedef BufferOutput = {
output:String,
terminator:String
};
/**
Helper class for reading/parsing information from a string buffer. Completely drops comments
**/
@:allow(tests.HankBufferTest)
class HankBuffer {
var path:String;
var cleanBuffer:String;
var rawBuffer:String;
var line:Int;
var column:Int;
public function new(path:String, rawBuffer:String, line:Int = 1, column:Int = 1) {
if (rawBuffer == null) {
throw 'Tried to create buffer of path $path with null contents: $rawBuffer';
}
this.path = path;
this.rawBuffer = rawBuffer;
// Keep a clean buffer for returning data without comments getting in the way
this.cleanBuffer = stripComments(rawBuffer, '//', '\n', false);
this.cleanBuffer = stripComments(cleanBuffer, '/*', '*/', true);
this.line = line;
this.column = column;
}
// TODO because this obfuscates the position of parsing, maybe it should be deprecated
public static function Dummy(text:String) {
return new HankBuffer('_', text, 1, 1);
}
public static function FromFile(path:String, ?files:PreloadedFiles) {
// Keep a raw buffer of the file for tracking accurate file positions
#if (sys || hxnodejs)
var rawBuffer = sys.io.File.getContent(path);
#else
if (files == null) {
throw 'Tried to open file $path on a non-sys platform without passing in preloaded files';
} else if (!files.exists(path)) {
throw 'Tried to open file $path that was not pre-loaded';
}
var rawBuffer = files[path];
#end
return new HankBuffer(path, rawBuffer);
}
public function lines():Array<String> {
var lines = cleanBuffer.split('\n');
drop(cleanBuffer);
return lines;
}
function stripComments(s:String, o:String, c:String, dc:Bool):String {
while (s.indexOf(o) != -1) {
var start = s.indexOf(o);
var end = s.indexOf(c, start);
if (end == -1) {
s = s.substr(0, start);
} else {
if (dc)
end += c.length;
s = s.substr(0, start) + s.substr(end);
}
}
return s;
}
public function indexOf(s:String, start:Int = 0):Int {
return cleanBuffer.indexOf(s, start);
}
public function everyIndexOf(s:String):Array<Int> {
return [for (i in 0...cleanBuffer.length) i].filter(function(i) return cleanBuffer.charAt(i) == s);
}
public function everyRootIndexOf(s:String) {
return [for (i in everyIndexOf(s)) i].filter(function(i) return depthAtIndex('{', '}', i) == 0);
}
public function rootIndexOf(s:String) {
// The DRYest possible implementation causes the program to hang when files are big:
// return everyRootIndexOf(s)[0];
var start = 0;
while (true) {
start = indexOf(s, start);
if (start == -1)
return -1;
if (depthAtIndex('{', '}', start) == 0) {
return start;
}
start += 1;
}
}
public function rootSplit(delimiter:String):Array<String> {
var rootIndices = everyRootIndexOf(delimiter);
if (rootIndices.length == 0) {
return [cleanBuffer];
}
var substrs = [];
var lastIdx = 0;
for (i in rootIndices) {
substrs.push(cleanBuffer.substr(lastIdx, i - lastIdx));
lastIdx = i + 1;
}
substrs.push(cleanBuffer.substr(lastIdx));
return substrs;
}
public function length():Int {
return cleanBuffer.length;
}
public function position():Position {
return new Position(path, line, column);
}
/** Peek at contents buffer waiting further ahead in the buffer **/
public function peekAhead(start:Int, length:Int):String {
return cleanBuffer.substr(start, length);
}
/** Peek through the buffer until encountering one of the given terminator sequences
@param eofTerminates Whether the end of the file is also a valid terminator
**/
public function peekUntil(terminators:Array<String>, eofTerminates:Bool = false, raw:Bool = false):Option<BufferOutput> {
var buffer = raw ? rawBuffer : cleanBuffer;
if (buffer.length == 0)
return None;
var index = buffer.length;
var whichTerminator = '';
for (terminator in terminators) {
var nextIndex = buffer.indexOf(terminator);
if (nextIndex != -1 && nextIndex < index) {
index = nextIndex;
whichTerminator = terminator;
}
}
return if (index < buffer.length || eofTerminates) {
Some({
output: buffer.substr(0, index),
terminator: whichTerminator
});
} else {
None;
}
}
/**
Drop the given string from the front of the raw buffer, updating the current position according to the raw buffer
**/
function dropRaw(s:String) {
var actual = rawBuffer.substr(0, s.length);
if (actual != s) {
throw 'Expected to drop "${s}" but was "${actual}"';
}
var lines = s.split('\n');
if (lines.length > 1) {
line += lines.length - 1;
column = lines[lines.length - 1].length + 1;
} else {
column += lines[0].length;
}
rawBuffer = rawBuffer.substr(s.length);
}
/** Drop text directly from the clean buffer **/
function dropClean(s:String) {
var actual = cleanBuffer.substr(0, s.length);
if (actual != s) {
throw 'Expected to drop "${s}" but was "${actual}"';
}
cleanBuffer = cleanBuffer.substr(s.length);
}
/**
Drop the given string from the front of the unified buffer object, keeping the two back-end buffers synchronized
**/
public function drop(s:String) {
var untilNextComment = peekUntil(['//', '/*'], false, true);
switch (untilNextComment) {
case Some({output: rawS, terminator: commentOpener}) if (rawS.length < s.length):
var commentTerminator = switch (commentOpener) {
case '//': '\n';
case '/*': '*/';
default: throw 'FUBAR';
}
dropRaw(rawS + commentOpener);
dropClean(rawS);
var untilEndOfComment = peekUntil([commentTerminator], true, true);
switch (untilEndOfComment) {
case Some({output: comment, terminator: _}):
dropRaw(comment);
if (commentTerminator != '\n') {
dropRaw(commentTerminator);
}
// Drop the rest of the clean sequence
var rest = s.substr(rawS.length);
drop(rest);
default: throw 'FUBAR';
}
default:
dropClean(s);
dropRaw(s);
}
}
/** Take data from the file until encountering one of the given terminator sequences. **/
public function takeUntil(terminators:Array<String>, eofTerminates:Bool = false, dropTerminator = true):Option<BufferOutput> {
return switch (peekUntil(terminators, eofTerminates)) {
case Some({output: s, terminator: t}):
// Remove the desired data from the buffer
drop(s);
// Remove the terminator that followed the data from the buffer
if (dropTerminator) {
drop(t);
}
// Return the desired data
Some({output: s, terminator: t});
case None:
None;
}
}
public function peek(chars:Int) {
if (cleanBuffer.length < chars) {
throw 'Not enough characters left in buffer.';
}
var data = cleanBuffer.substr(0, chars);
return data;
}
public function take(chars:Int) {
var data = peek(chars);
drop(data);
return data;
}
/** Count consecutive occurrence of the given string at the current buffer position, dropping the counted sequence **/
public function countConsecutive(s:String) {
var num = 0;
while (cleanBuffer.substr(0, s.length) == s) {
num += 1;
drop(s);
}
return num;
}
/** If the given expression comes next in the buffer, take its contents. Otherwise, return None **/
public function expressionIfNext(o:String, c:String):Option<String> {
if (cleanBuffer.startsWith(o) && cleanBuffer.indexOf(c) != -1) {
drop(o);
var end = cleanBuffer.indexOf(c);
var content = take(end);
drop(c);
return Some(content);
}
return None;
}
/** DRY Helper for peekLine() and takeLine() **/
function getLine(trimmed:String, retriever:Array<String>->Bool->Bool->Option<BufferOutput>, dropNewline:Bool):Option<String> {
var nextLine = retriever(['\n'], true, false);
return switch (nextLine) {
case Some({output: nextLine, terminator: _}):
if (dropNewline && !isEmpty()) {
drop('\n');
}
if (trimmed.indexOf('r') != -1) {
nextLine = nextLine.rtrim();
}
if (trimmed.indexOf('l') != -1) {
nextLine = nextLine.ltrim();
}
Some(nextLine);
case None:
None;
};
}
/** Peek the next line of data from the file.
@param trimmed Which sides of the line to trim ('r' 'l', 'lr', or 'rl')
**/
public function peekLine(trimmed = ''):Option<String> {
return getLine(trimmed, peekUntil, false);
}
/** Take the next line of data from the file.
@param trimmed Which sides of the line to trim ('r' 'l', 'lr', or 'rl')
**/
public function takeLine(trimmed = ''):Option<String> {
return getLine(trimmed, takeUntil, true);
}
public function skipWhitespace(terminator:String = '') {
var nextTerm = cleanBuffer.indexOf(terminator);
var withoutTerm = cleanBuffer.length - cleanBuffer.ltrim().length;
var end = if (nextTerm <= 0) withoutTerm else Math.floor(Math.min(nextTerm, withoutTerm));
var whitespace = cleanBuffer.substr(0, end);
drop(whitespace);
}
public function isEmpty() {
return cleanBuffer.length == 0;
}
/**
By counting matched pairs of o and c, find out the nesting depth of the char at the given index
**/
public function depthAtIndex(o:String, c:String, index:Int) {
var depth = 0;
var snippet = cleanBuffer.substr(0, index);
for (i in 0...snippet.length) {
var whichC = snippet.charAt(i);
if (whichC == o) {
depth += 1;
} else if (whichC == c) {
depth -= 1;
}
}
return depth;
}
/** Return the start index and length of number of characters left the buffer before a nestable expression terminates **/
public function findNestedExpression(o:String, c:String, start:Int = 0, throwExceptions:Bool = true):Option<BufferSlice> {
var startIdx = start;
var endIdx = start;
var depth = 0;
var nextIdx = start;
do {
var nextOpeningIdx = cleanBuffer.indexOf(o, nextIdx);
var nextClosingIdx = cleanBuffer.indexOf(c, nextIdx);
if (nextOpeningIdx == -1 && nextClosingIdx == -1) {
return None;
} else if (depth == 0 && nextOpeningIdx == -1) {
if (throwExceptions)
throw 'Your input file $path has an expression with an unmatched closing operator $c';
else
return None;
} else if (depth != 0 && nextClosingIdx == -1) {
if (throwExceptions)
throw 'Your input file $path has an expression with an unmatched opening operator $o';
else
return None;
} else if (nextOpeningIdx != -1 && nextOpeningIdx < nextClosingIdx) {
if (depth == 0) {
startIdx = nextOpeningIdx;
}
depth += 1;
nextIdx = nextOpeningIdx + o.length;
} else {
depth -= 1;
nextIdx = nextClosingIdx + c.length;
endIdx = nextClosingIdx + c.length;
}
} while (depth > 0 && nextIdx < cleanBuffer.length);
return Some(new BufferSlice(startIdx, endIdx - startIdx, this));
}
}