@bitblit/asyncglk
Version:
A Typescript Glk library
277 lines (273 loc) • 8.28 kB
JavaScript
/*
Glk Streams
===========
Copyright (c) 2023 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import { is_unicode_array } from '../common/misc.js';
import { filemode_Write, seekmode_Current, seekmode_End } from './constants.js';
import { FileRef } from './filerefs.js';
import { GLK_NULL, MAX_LATIN1, QUESTION_MARK } from './lib_constants.js';
class StreamBase {
disprock;
fmode;
next = null;
prev = null;
rock;
write_count = 0;
constructor(fmode, rock) {
this.fmode = fmode;
this.rock = rock;
}
close(result) {
if (result) {
result.set_field(0, 0);
result.set_field(1, this.write_count);
}
}
get_buffer(_buf) {
return 0;
}
get_char(_unicode) {
return -1;
}
get_line(_buf) {
return 0;
}
get_position() {
return 0;
}
set_position(_mode, _pos) { }
}
/** A fixed-length TypedArray backed stream */
export class ArrayBackedStream extends StreamBase {
buf;
close_cb;
/** Whether we need to check if we should expand the buffer before writing */
expandable;
/** The length of the active region of the buffer.
* This can be shorter than the actual length of the buffer in two situations: a file stream, or a `filemode_Write` memory stream.
* See https://github.com/iftechfoundation/ifarchive-if-specs/issues/8
*/
len;
pos = 0;
read_count = 0;
type = 'array';
uni;
constructor(buf, fmode, rock, close_cb) {
super(fmode, rock);
this.buf = buf;
this.close_cb = close_cb;
this.expandable = fmode === filemode_Write;
this.len = fmode === filemode_Write ? 0 : buf.length;
this.uni = is_unicode_array(buf);
}
close(result) {
this.close_cb?.();
this.write();
if (result) {
result.set_field(0, this.read_count);
result.set_field(1, this.write_count);
}
}
expand(increase) {
this.len = Math.min(this.pos + increase, this.buf.length);
if (this.len === this.buf.length) {
this.expandable = false;
}
}
get_buffer(buf) {
const read_length = Math.min(buf.length, this.len - this.pos);
// When a unicode array is read into a latin1 array we must catch non-latin1 characters
if (!is_unicode_array(buf) && this.uni) {
for (let i = 0; i < read_length; i++) {
const ch = this.buf[this.pos + i];
buf[i] = ch > MAX_LATIN1 ? QUESTION_MARK : ch;
}
}
// Otherwise we can copy it whole
else {
buf.set(this.buf.subarray(this.pos, this.pos + read_length));
}
this.pos += read_length;
this.read_count += read_length;
return read_length;
}
get_char(unicode) {
if (this.pos < this.len) {
this.read_count++;
const ch = this.buf[this.pos++];
return !unicode && ch > MAX_LATIN1 ? QUESTION_MARK : ch;
}
return -1;
}
get_line(buf) {
const read_length = Math.min(buf.length - 1, this.len - this.pos);
if (read_length < 0) {
return 0;
}
const check_unicode = !is_unicode_array(buf) && this.uni;
let i = 0;
while (i < read_length) {
const ch = this.buf[this.pos++];
buf[i++] = check_unicode && ch > MAX_LATIN1 ? QUESTION_MARK : ch;
if (ch === 10) {
break;
}
}
buf[i] = GLK_NULL;
this.read_count += i;
return i;
}
get_position() {
return this.pos;
}
put_buffer(buf, uni) {
const buf_length = buf.length;
this.write_count += buf_length;
if (this.pos + buf_length > this.len && this.expandable) {
this.expand(buf_length);
}
const write_length = Math.min(buf_length, this.len - this.pos);
if (write_length === 0) {
return;
}
// When writing a unicode array into a latin1 array we must catch non-latin1 characters
if (uni && !this.uni) {
for (let i = 0; i < write_length; i++) {
const ch = buf[i];
this.buf[this.pos + i] = ch > MAX_LATIN1 ? QUESTION_MARK : ch;
}
}
// Otherwise we can copy it whole
else {
if (Array.isArray(buf)) {
buf = (uni ? Uint32Array : Uint8Array).from(buf);
}
this.buf.set(buf.subarray(0, write_length), this.pos);
}
this.pos += write_length;
this.write();
}
put_char(ch) {
this.write_count++;
if (this.pos === this.len && this.expandable) {
this.expand(1);
}
if (this.pos < this.len) {
this.buf[this.pos++] = (!this.uni && ch > MAX_LATIN1) ? QUESTION_MARK : ch;
this.write();
}
}
put_string(str) {
this.put_buffer(Uint32Array.from(str, ch => ch.codePointAt(0)), true);
}
set_position(mode, pos) {
if (mode === seekmode_Current) {
this.pos += pos;
}
else if (mode === seekmode_End) {
this.pos = this.len + pos;
}
// Default to start if the mode is invalid
else {
this.pos = pos;
}
if (this.pos < 0) {
this.pos = 0;
}
if (this.pos > this.len) {
this.pos = this.len;
}
}
write() { }
}
/** FileStreams are based on array backed streams, but can grow in length */
export class FileStream extends ArrayBackedStream {
fref;
constructor(fref, buf, fmode, rock) {
super(buf, fmode, rock);
this.expandable = true;
this.fref = fref;
}
expand(increase) {
const end_pos = this.pos + increase;
this.len = end_pos;
let maxlen = this.buf.length;
if (end_pos > maxlen) {
// Always expand by at least 100
maxlen += Math.max(end_pos - maxlen, 100);
const old_buf = this.buf;
this.buf = new (this.uni ? Uint32Array : Uint8Array)(maxlen);
this.buf.set(old_buf);
}
}
write() {
this.fref.write(this.buf.subarray(0, this.len));
}
}
/** A null stream */
export class NullStream extends StreamBase {
type = 'null';
put_buffer(buf, _uni) {
this.write_count += buf.length;
}
put_char(_ch) {
this.write_count++;
}
put_string(str, _style) {
this.write_count += [...str].length;
}
}
/** Window streams operate a little bit differently */
export class WindowStream extends StreamBase {
type = 'window';
win;
constructor(win) {
super(filemode_Write, 0);
this.win = win;
}
put_buffer(buf, uni) {
if (this.win.input.type === 'line') {
throw new Error('Window has pending line request');
}
this.write_count += buf.length;
this.win.put_string(String.fromCodePoint(...buf));
this.win.echostr?.put_buffer(buf, uni);
}
put_char(ch) {
if (this.win.input.type === 'line') {
throw new Error('Window has pending line request');
}
this.write_count++;
this.win.put_string(String.fromCodePoint(ch));
this.win.echostr?.put_char(ch);
}
put_string(str, style) {
if (this.win.input.type === 'line') {
throw new Error('Window has pending line request');
}
this.write_count += [...str].length;
this.win.put_string(str, style);
this.win.echostr?.put_string(str, style);
}
set_css(name, val) {
this.win.set_css(name, val);
if (this.win.echostr?.type === 'window') {
this.win.echostr.set_css(name, val);
}
}
set_hyperlink(val) {
this.win.set_hyperlink(val);
if (this.win.echostr?.type === 'window') {
this.win.echostr.set_hyperlink(val);
}
}
set_style(style) {
this.win.set_style(style);
if (this.win.echostr?.type === 'window') {
this.win.echostr.set_style(style);
}
}
}