UNPKG

@bitblit/asyncglk

Version:
277 lines (273 loc) 8.28 kB
/* 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); } } }