preserves/implementations/javascript/packages/core/src/writer.ts

336 lines
11 KiB
TypeScript

import { isAnnotated } from './is';
import { Record, Tuple } from "./record";
import type { GenericEmbedded, Embedded, EmbeddedTypeEncode } from "./embedded";
import { Encoder, EncoderState } from "./encoder";
import type { Value } from "./values";
import { NUMBER_RE } from './reader';
export type Writable<T> =
Value<T> | PreserveWritable<T> | Iterable<Value<T>> | ArrayBufferView;
export interface PreserveWritable<T> {
__preserve_text_on__(writer: Writer<T>): void;
}
export function isPreserveWritable<T>(v: any): v is PreserveWritable<T> {
return typeof v === 'object' && v !== null && '__preserve_text_on__' in v && typeof v.__preserve_text_on__ === 'function';
}
function isIterable<T>(v: any): v is Iterable<T> {
return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function';
}
export type EmbeddedWriter<T> =
{ write(s: WriterState, v: T): void } | { toValue(v: T): Value<GenericEmbedded> };
export const genericEmbeddedTypeEncode: EmbeddedTypeEncode<GenericEmbedded> & EmbeddedWriter<GenericEmbedded> = {
encode(s: EncoderState, v: GenericEmbedded): void {
new Encoder(s, this).push(v.generic);
},
toValue(v: GenericEmbedded): Value<GenericEmbedded> {
return v.generic;
}
};
export const neverEmbeddedTypeEncode: EmbeddedTypeEncode<never> & EmbeddedWriter<never> = {
encode(_s: EncoderState, _v: never): void {
throw new Error("Embeddeds not permitted encoding Preserves document");
},
toValue(_v: never): Value<GenericEmbedded> {
throw new Error("Embeddeds not permitted writing Preserves document");
}
};
export interface WriterStateOptions {
includeAnnotations?: boolean;
indent?: number;
maxBinaryAsciiLength?: number;
maxBinaryAsciiProportion?: number;
}
export interface WriterOptions<T> extends WriterStateOptions {
embeddedWrite?: EmbeddedWriter<T>;
}
export class WriterState {
pieces: string[] = [];
options: WriterStateOptions;
indentDelta: string;
indentCount = 0;
constructor (options: WriterStateOptions) {
this.options = options;
this.indentDelta = ' '.repeat(options.indent ?? 0);
}
get isIndenting(): boolean {
return this.indentDelta.length > 0;
}
get includeAnnotations(): boolean {
return this.options.includeAnnotations ?? true;
}
writeIndent() {
if (this.isIndenting) {
this.pieces.push('\n');
for (let i = 0; i < this.indentCount; i++) {
this.pieces.push(this.indentDelta);
}
}
}
writeIndentSpace() {
if (this.isIndenting) {
this.writeIndent();
} else {
this.pieces.push(' ');
}
}
escapeStringlikeChar(c: string, k: (c: string) => string = (c) => c): string {
switch (c) {
case "\\": return "\\\\";
case "\x08": return "\\b";
case "\x0c": return "\\f";
case "\x0a": return "\\n";
case "\x0d": return "\\r";
case "\x09": return "\\t";
default: return k(c);
}
}
escapeStringlike(s: string, quoteChar: string): string {
let buf = quoteChar;
for (let c of s) {
buf = buf + ((c === quoteChar) ? "\\" + quoteChar : this.escapeStringlikeChar(c));
}
return buf + quoteChar;
}
writeSeq<V>(opener: string, closer: string, vs: Iterable<V>, appender: (v: V) => void) {
let iter = vs[Symbol.iterator]();
this.pieces.push(opener);
const first_i = iter.next();
if (first_i.done !== true) {
const first_v = first_i.value;
const second_i = iter.next();
if (second_i.done === true) {
appender(first_v);
} else {
this.indentCount++;
this.writeIndent();
appender(first_v);
this.writeIndentSpace();
appender(second_i.value);
let i: IteratorResult<V>;
while ((i = iter.next()).done !== true) {
this.writeIndentSpace();
appender(i.value);
}
this.indentCount--;
this.writeIndent();
}
}
this.pieces.push(closer);
}
writeBytes(bs: Uint8Array) {
const limit = this.options.maxBinaryAsciiLength ?? 1024;
const proportion = this.options.maxBinaryAsciiProportion ?? 0.75;
if (bs.length >= limit) {
this.writeBase64(bs);
} else {
let count = 0;
let sampleSize = Math.min(bs.length, limit);
for (let i = 0; i < sampleSize; i++) {
const b = bs[i];
switch (b) {
case 9:
case 10:
case 13:
count++;
break;
default:
if (b >= 32 && b <= 126) {
count++;
}
break;
}
}
if (sampleSize === 0 || (count / sampleSize) >= proportion) {
this.writeBinaryStringlike(bs);
} else {
this.writeBase64(bs);
}
}
}
writeBase64(bs: Uint8Array) {
this.pieces.push("#[", encodeBase64(bs), "]");
}
writeBinaryStringlike(bs: Uint8Array) {
let buf = '#"';
for (let b of bs) {
if (b === 0x22) {
buf = buf + '\\"';
} else {
buf = buf + this.escapeStringlikeChar(String.fromCharCode(b), c => {
if ((b >= 0x20 && b <= 0x7e) && (b !== 0x5c)) {
return c;
} else {
return '\\x' + ('0' + b.toString(16)).slice(-2);
}
});
}
}
this.pieces.push(buf + '"');
}
couldBeFlat<T>(vs: Writable<T>[]): boolean {
let seenCompound = false;
for (let v of vs) {
if (Array.isArray(v) || Set.isSet(v) || Map.isMap(v)) {
if (seenCompound) return false;
seenCompound = true;
}
if (this.includeAnnotations && isAnnotated(v) && v.annotations.length > 1) {
return false;
}
}
return true;
}
}
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
export function encodeBase64(bs: Uint8Array): string {
let s = '';
let buffer = 0;
let bitcount = 0;
for (let b of bs) {
buffer = ((buffer & 0x3f) << 8) | b;
bitcount += 8;
while (bitcount >= 6) {
bitcount -= 6;
const v = (buffer >> bitcount) & 0x3f;
s = s + BASE64[v];
}
}
if (bitcount > 0) {
const v = (buffer << (6 - bitcount)) & 0x3f;
s = s + BASE64[v];
}
return s;
}
export class Writer<T> {
state: WriterState;
embeddedWrite: EmbeddedWriter<T>;
constructor(state: WriterState, embeddedWrite: EmbeddedWriter<T>);
constructor(options?: WriterOptions<T>);
constructor(
state_or_options: (WriterState | WriterOptions<T>) = {},
embeddedWrite?: EmbeddedWriter<T>
) {
if (state_or_options instanceof WriterState) {
this.state = state_or_options;
this.embeddedWrite = embeddedWrite!;
} else {
this.state = new WriterState(state_or_options);
this.embeddedWrite = state_or_options.embeddedWrite ?? neverEmbeddedTypeEncode;
}
}
static stringify<T>(v: Writable<T>, options?: WriterOptions<T>): string {
const w = new Writer(options);
w.push(v);
return w.contents();
}
contents(): string {
return this.state.pieces.join('');
}
get includeAnnotations(): boolean {
return this.state.includeAnnotations;
}
push(v: Writable<T>) {
switch (typeof v) {
case 'boolean':
this.state.pieces.push(v ? '#t' : '#f');
break;
case 'string':
this.state.pieces.push(this.state.escapeStringlike(v, '"'));
break;
case 'symbol': {
const s = v.description!;
// FIXME: This regular expression is conservatively correct, but Anglo-chauvinistic.
if (/^[-a-zA-Z0-9~!$%^&*?_=+/.]+$/.exec(s) && !NUMBER_RE.exec(s)) {
this.state.pieces.push(s);
} else {
this.state.pieces.push(this.state.escapeStringlike(s, '|'));
}
break;
}
case 'number':
this.state.pieces.push('' + v);
break;
case 'object':
if (isPreserveWritable<unknown>(v)) {
v.__preserve_text_on__(this);
}
else if (isPreserveWritable<T>(v)) {
v.__preserve_text_on__(this);
}
else if (ArrayBuffer.isView(v)) {
if (v instanceof Uint8Array) {
this.state.writeBytes(v);
} else {
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
this.state.writeBytes(bs);
}
}
else if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
const flat = this.state.couldBeFlat(v);
this.state.pieces.push('<');
this.push(v.label);
if (!flat) this.state.indentCount++;
for (let i of v) {
if (flat) {
this.state.pieces.push(' ');
} else {
this.state.writeIndentSpace();
}
this.push(i);
}
if (!flat) this.state.indentCount--;
this.state.pieces.push('>');
}
else if (isIterable(v)) {
this.state.writeSeq('[', ']', v, vv => this.push(vv));
}
else {
((v: Embedded<T>) => {
this.state.pieces.push('#!');
if ('write' in this.embeddedWrite) {
this.embeddedWrite.write(this.state, v.embeddedValue);
} else {
new Writer(this.state, genericEmbeddedTypeEncode)
.push(this.embeddedWrite.toValue(v.embeddedValue));
}
})(v);
}
break;
default:
throw new Error(`Internal error: unhandled in Preserves Writer.push for ${v}`);
}
return this; // for chaining
}
}