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 = Value | PreserveWritable | Iterable> | ArrayBufferView; export interface PreserveWritable { __preserve_text_on__(writer: Writer): void; } export function isPreserveWritable(v: any): v is PreserveWritable { return typeof v === 'object' && v !== null && '__preserve_text_on__' in v && typeof v.__preserve_text_on__ === 'function'; } function isIterable(v: any): v is Iterable { return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; } export type EmbeddedWriter = { write(s: WriterState, v: T): void } | { toValue(v: T): Value }; export const genericEmbeddedTypeEncode: EmbeddedTypeEncode & EmbeddedWriter = { encode(s: EncoderState, v: GenericEmbedded): void { new Encoder(s, this).push(v.generic); }, toValue(v: GenericEmbedded): Value { return v.generic; } }; export const neverEmbeddedTypeEncode: EmbeddedTypeEncode & EmbeddedWriter = { encode(_s: EncoderState, _v: never): void { throw new Error("Embeddeds not permitted encoding Preserves document"); }, toValue(_v: never): Value { throw new Error("Embeddeds not permitted writing Preserves document"); } }; export interface WriterStateOptions { includeAnnotations?: boolean; indent?: number; maxBinaryAsciiLength?: number; maxBinaryAsciiProportion?: number; } export interface WriterOptions extends WriterStateOptions { embeddedWrite?: EmbeddedWriter; } 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(opener: string, closer: string, vs: Iterable, 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; 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(vs: Writable[]): 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 { state: WriterState; embeddedWrite: EmbeddedWriter; constructor(state: WriterState, embeddedWrite: EmbeddedWriter); constructor(options?: WriterOptions); constructor( state_or_options: (WriterState | WriterOptions) = {}, embeddedWrite?: EmbeddedWriter ) { 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(v: Writable, options?: WriterOptions): 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) { 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(v)) { v.__preserve_text_on__(this); } else if (isPreserveWritable(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, Tuple>, 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) => { 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 } }