diff --git a/implementations/javascript/packages/core/src/annotated.ts b/implementations/javascript/packages/core/src/annotated.ts index d97bcf8..d7eebe4 100644 --- a/implementations/javascript/packages/core/src/annotated.ts +++ b/implementations/javascript/packages/core/src/annotated.ts @@ -1,10 +1,9 @@ -import { Encoder } from "./encoder"; import { Tag } from "./constants"; -import { Value } from "./values"; import { is, isAnnotated, IsPreservesAnnotated } from "./is"; -import { stringify } from "./text"; -import { GenericEmbedded } from "./embedded"; -import type { Preservable } from "./encoder"; +import type { GenericEmbedded } from "./embedded"; +import type { Value } from "./values"; +import type { Encoder, Preservable } from "./encoder"; +import type { Writer, PreserveWritable } from "./writer"; export interface Position { line?: number; @@ -53,7 +52,7 @@ export function formatPosition(p: Position | null | string): string { } } -export class Annotated implements Preservable { +export class Annotated implements Preservable, PreserveWritable { readonly annotations: Array>; readonly pos: Position | null; readonly item: Value; @@ -82,6 +81,22 @@ export class Annotated implements Preservable { encoder.push(this.item); } + __preserve_text_on__(w: Writer): void { + if (w.includeAnnotations) { + const flat = this.annotations.length <= 1; + for (const a of this.annotations) { + w.state.pieces.push("@"); + w.push(a); + if (flat) { + w.state.pieces.push(" "); + } else { + w.state.writeIndentSpace(); + } + } + } + w.push(this.item); + } + equals(other: any): boolean { return is(this.item, Annotated.isAnnotated(other) ? other.item : other); } @@ -90,15 +105,6 @@ export class Annotated implements Preservable { // return hash(this.item); // } - toString(): string { - return this.asPreservesText(); - } - - asPreservesText(): string { - const anns = this.annotations.map((a) => '@' + stringify(a)).join(' '); - return (anns ? anns + ' ' : anns) + stringify(this.item); - } - get [IsPreservesAnnotated](): boolean { return true; } diff --git a/implementations/javascript/packages/core/src/bytes.ts b/implementations/javascript/packages/core/src/bytes.ts index b7769e0..40cae0b 100644 --- a/implementations/javascript/packages/core/src/bytes.ts +++ b/implementations/javascript/packages/core/src/bytes.ts @@ -1,7 +1,8 @@ import { Tag } from './constants'; +import { GenericEmbedded } from './embedded'; import { Encoder, Preservable } from './encoder'; import { Value } from './values'; -import { GenericEmbedded } from './embedded'; +import { Writer, PreserveWritable } from './writer'; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); @@ -10,7 +11,7 @@ export const IsPreservesBytes = Symbol.for('IsPreservesBytes'); export type BytesLike = Bytes | Uint8Array; -export class Bytes implements Preservable { +export class Bytes implements Preservable, PreserveWritable { readonly _view: Uint8Array; constructor(maybeByteIterable: any = new Uint8Array()) { @@ -122,10 +123,6 @@ export class Bytes implements Preservable { return textDecoder.decode(this._view); } - toString(): string { - return this.asPreservesText(); - } - __as_preserve__(): Value { return this; } @@ -134,26 +131,6 @@ export class Bytes implements Preservable { return Bytes.isBytes(v) ? v : void 0; } - asPreservesText(): string { - return '#"' + this.__asciify() + '"'; - } - - __asciify(): string { - const pieces = []; - const v = this._view; - for (let i = 0; i < v.length; i++) { - const b = v[i]; - if (b === 92 || b === 34) { - pieces.push('\\' + String.fromCharCode(b)); - } else if (b >= 32 && b <= 126) { - pieces.push(String.fromCharCode(b)); - } else { - pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15)); - } - } - return pieces.join(''); - } - toHex(): string { var nibbles = []; for (let i = 0; i < this.length; i++) { @@ -168,12 +145,16 @@ export class Bytes implements Preservable { return this.toHex(); } - __preserve_on__(encoder: Encoder) { + __preserve_on__(encoder: Encoder) { encoder.state.emitbyte(Tag.ByteString); encoder.state.varint(this.length); encoder.state.emitbytes(this._view); } + __preserve_text_on__(w: Writer) { + w.state.writeBytes(this._view); + } + get [IsPreservesBytes](): boolean { return true; } diff --git a/implementations/javascript/packages/core/src/dictionary.ts b/implementations/javascript/packages/core/src/dictionary.ts index ccc4e66..a2db039 100644 --- a/implementations/javascript/packages/core/src/dictionary.ts +++ b/implementations/javascript/packages/core/src/dictionary.ts @@ -1,16 +1,19 @@ import { Encoder, canonicalEncode, canonicalString } from "./encoder"; import { Tag } from "./constants"; import { FlexMap, FlexSet, _iterMap } from "./flex"; -import { stringify } from "./text"; import { Value } from "./values"; import { Bytes } from './bytes'; import { GenericEmbedded } from "./embedded"; import type { Preservable } from "./encoder"; +import type { Writer, PreserveWritable } from "./writer"; +import { annotations, Annotated } from "./annotated"; export type DictionaryType = 'Dictionary' | 'Set'; export const DictionaryType = Symbol.for('DictionaryType'); -export class KeyedDictionary, V, T = GenericEmbedded> extends FlexMap implements Preservable { +export class KeyedDictionary, V, T = GenericEmbedded> extends FlexMap + implements Preservable, PreserveWritable +{ get [DictionaryType](): DictionaryType { return 'Dictionary'; } @@ -34,21 +37,10 @@ export class KeyedDictionary, V, T = GenericEmbedded> extends return result; } - asPreservesText(): string { - return '{' + - Array.from(_iterMap(this.entries(), ([k, v]) => - stringify(k) + ': ' + stringify(v))).join(', ') + - '}'; - } - clone(): KeyedDictionary { return new KeyedDictionary(this); } - toString(): string { - return this.asPreservesText(); - } - get [Symbol.toStringTag]() { return 'Dictionary'; } __preserve_on__(encoder: Encoder) { @@ -72,6 +64,22 @@ export class KeyedDictionary, V, T = GenericEmbedded> extends encoder.state.emitbyte(Tag.End); } } + + __preserve_text_on__(w: Writer) { + w.state.writeSeq('{', '}', this.entries(), ([k, v]) => { + w.push(k); + if (Annotated.isAnnotated(v) && (annotations(v).length > 1) && w.state.isIndenting) { + w.state.pieces.push(':'); + w.state.indentCount++; + w.state.writeIndent(); + w.push(v); + w.state.indentCount--; + } else { + w.state.pieces.push(': '); + w.push(v as unknown as Value); // Suuuuuuuper unsound + } + }); + } } export class Dictionary> extends KeyedDictionary, V, T> { @@ -84,7 +92,9 @@ export class Dictionary> extends KeyedDictiona } } -export class KeyedSet, T = GenericEmbedded> extends FlexSet implements Preservable { +export class KeyedSet, T = GenericEmbedded> extends FlexSet + implements Preservable, PreserveWritable +{ get [DictionaryType](): DictionaryType { return 'Set'; } @@ -107,16 +117,6 @@ export class KeyedSet, T = GenericEmbedded> extends FlexSet { return new KeyedSet(this); } @@ -132,6 +132,10 @@ export class KeyedSet, T = GenericEmbedded> extends FlexSet) { + w.state.writeSeq('#{', '}', this, vv => w.push(vv)); + } } export class Set extends KeyedSet, T> { diff --git a/implementations/javascript/packages/core/src/embedded.ts b/implementations/javascript/packages/core/src/embedded.ts index bfbebdf..705b36d 100644 --- a/implementations/javascript/packages/core/src/embedded.ts +++ b/implementations/javascript/packages/core/src/embedded.ts @@ -1,5 +1,5 @@ -import type { EncoderState } from "./encoder"; import type { DecoderState } from "./decoder"; +import type { EncoderState } from "./encoder"; import type { Value } from "./values"; import { ReaderStateOptions } from "./reader"; @@ -26,12 +26,8 @@ export class Embedded { return isEmbedded(other) && is(this.embeddedValue, other.embeddedValue); } - asPreservesText(): string { - try { - return '#!' + (this.embeddedValue as any).asPreservesText(); - } catch { - return '#!' + (this.embeddedValue as any).toString(); - } + toString(): string { + return '#!' + (this.embeddedValue as any).toString(); } __as_preserve__(): T extends R ? Value : never { @@ -62,7 +58,7 @@ export class GenericEmbedded { return typeof other === 'object' && 'generic' in other && is(this.generic, other.generic); } - asPreservesText(): string { - return this.generic.asPreservesText(); + toString(): string { + return this.generic.toString(); } } diff --git a/implementations/javascript/packages/core/src/embeddedTypes.ts b/implementations/javascript/packages/core/src/embeddedTypes.ts index 6ce685c..7a86669 100644 --- a/implementations/javascript/packages/core/src/embeddedTypes.ts +++ b/implementations/javascript/packages/core/src/embeddedTypes.ts @@ -1,34 +1,15 @@ -import { GenericEmbedded, EmbeddedType, EmbeddedTypeDecode, EmbeddedTypeEncode } from "./embedded"; -import { Encoder, EncoderState, identityEmbeddedTypeEncode } from "./encoder"; +import { identityEmbeddedTypeEncode } from "./encoder"; import { genericEmbeddedTypeDecode, ReaderStateOptions } from "./reader"; +import { genericEmbeddedTypeEncode, neverEmbeddedTypeEncode } from "./writer"; import { Value } from "./values"; import { DecoderState, neverEmbeddedTypeDecode } from "./decoder"; - -export const genericEmbeddedTypeEncode: EmbeddedTypeEncode = { - encode(s: EncoderState, v: GenericEmbedded): void { - new Encoder(s, this).push(v.generic); - }, - - toValue(v: GenericEmbedded): Value { - return v.generic; - } -}; +import type { GenericEmbedded, EmbeddedType, EmbeddedTypeDecode } from "./embedded"; export const genericEmbeddedType: EmbeddedType = Object.assign({}, genericEmbeddedTypeDecode, genericEmbeddedTypeEncode); -export const neverEmbeddedTypeEncode: EmbeddedTypeEncode = { - 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 encoding Preserves document"); - } -}; - export const neverEmbeddedType: EmbeddedType = Object.assign({}, neverEmbeddedTypeDecode, diff --git a/implementations/javascript/packages/core/src/encoder.ts b/implementations/javascript/packages/core/src/encoder.ts index fc0ccf6..a3ce94f 100644 --- a/implementations/javascript/packages/core/src/encoder.ts +++ b/implementations/javascript/packages/core/src/encoder.ts @@ -4,6 +4,7 @@ import { Value } from "./values"; import { EncodeError } from "./codec"; import { Record, Tuple } from "./record"; import { GenericEmbedded, EmbeddedTypeEncode } from "./embedded"; +import type { Embedded } from "./embedded"; export type Encodable = Value | Preservable | Iterable> | ArrayBufferView; @@ -248,15 +249,14 @@ export class Encoder { for (let i of v) { this.push(i); } this.state.emitbyte(Tag.End); } - else if (Array.isArray(v)) { + else if (isIterable>(v)) { this.encodevalues(Tag.Sequence, v); } - else if (isIterable>(v)) { - this.encodevalues(Tag.Sequence, v as Iterable>); - } else { - this.state.emitbyte(Tag.Embedded); - this.embeddedEncode.encode(this.state, v.embeddedValue); + ((v: Embedded) => { + this.state.emitbyte(Tag.Embedded); + this.embeddedEncode.encode(this.state, v.embeddedValue); + })(v); } return this; // for chaining } diff --git a/implementations/javascript/packages/core/src/float.ts b/implementations/javascript/packages/core/src/float.ts index 1b8c8c3..a831a44 100644 --- a/implementations/javascript/packages/core/src/float.ts +++ b/implementations/javascript/packages/core/src/float.ts @@ -1,7 +1,9 @@ -import { Encoder, Preservable } from "./encoder"; import { Tag } from "./constants"; +import { stringify } from "./text"; import { Value } from "./values"; -import { GenericEmbedded } from "./embedded"; +import type { GenericEmbedded } from "./embedded"; +import type { Encoder, Preservable } from "./encoder"; +import type { Writer, PreserveWritable } from "./writer"; export type FloatType = 'Single' | 'Double'; export const FloatType = Symbol.for('FloatType'); @@ -14,7 +16,7 @@ export abstract class Float { } toString() { - return this.asPreservesText(); + return stringify(this); } equals(other: any): boolean { @@ -25,7 +27,6 @@ export abstract class Float { return (this.value | 0); // TODO: something better? } - abstract asPreservesText(): string; abstract get [FloatType](): FloatType; static isFloat = (x: any): x is Float => x?.[FloatType] !== void 0; @@ -43,7 +44,7 @@ export function floatValue(f: any): number { } } -export class SingleFloat extends Float implements Preservable { +export class SingleFloat extends Float implements Preservable, PreserveWritable { __as_preserve__(): Value { return this; } @@ -59,12 +60,12 @@ export class SingleFloat extends Float implements Preservable { encoder.state.index += 4; } - get [FloatType](): 'Single' { - return 'Single'; + __preserve_text_on__(w: Writer) { + w.state.pieces.push('' + this.value + 'f'); } - asPreservesText(): string { - return '' + this.value + 'f'; + get [FloatType](): 'Single' { + return 'Single'; } } @@ -72,7 +73,7 @@ export function Single(value: number | Float): SingleFloat { return new SingleFloat(value); } -export class DoubleFloat extends Float implements Preservable { +export class DoubleFloat extends Float implements Preservable, PreserveWritable { __as_preserve__(): Value { return this; } @@ -88,12 +89,12 @@ export class DoubleFloat extends Float implements Preservable { encoder.state.index += 8; } - get [FloatType](): 'Double' { - return 'Double'; + __preserve_text_on__(w: Writer) { + w.state.pieces.push('' + this.value); } - asPreservesText(): string { - return '' + this.value; + get [FloatType](): 'Double' { + return 'Double'; } } diff --git a/implementations/javascript/packages/core/src/node_support.ts b/implementations/javascript/packages/core/src/node_support.ts index ebf15e3..8ed1c60 100644 --- a/implementations/javascript/packages/core/src/node_support.ts +++ b/implementations/javascript/packages/core/src/node_support.ts @@ -3,15 +3,13 @@ import { Annotated } from './annotated'; import { Bytes } from './bytes'; import { Set, Dictionary } from './dictionary'; -import { Record } from './record'; +import { stringify } from './text'; import * as util from 'util'; [Bytes, Annotated, Set, Dictionary].forEach((C) => { (C as any).prototype[util.inspect.custom] = function (_depth: any, _options: any) { - return this.asPreservesText(); + return stringify(this, { indent: 2 }); }; }); - -Record.fallbackToString = util.inspect; diff --git a/implementations/javascript/packages/core/src/reader.ts b/implementations/javascript/packages/core/src/reader.ts index 2133a94..220f1f7 100644 --- a/implementations/javascript/packages/core/src/reader.ts +++ b/implementations/javascript/packages/core/src/reader.ts @@ -59,7 +59,7 @@ export class ReaderState { if (this.atEnd()) { this.buffer = data; } else { - this.buffer = this.buffer.substr(this.index) + data; + this.buffer = this.buffer.substring(this.index) + data; } this.discarded += this.index; this.index = 0; diff --git a/implementations/javascript/packages/core/src/record.ts b/implementations/javascript/packages/core/src/record.ts index 83fb64d..eb94f0a 100644 --- a/implementations/javascript/packages/core/src/record.ts +++ b/implementations/javascript/packages/core/src/record.ts @@ -1,6 +1,7 @@ import { GenericEmbedded } from "./embedded"; import { is } from "./is"; import { Value } from "./values"; +import { Writer } from "./writer"; export type Tuple = Array | [T]; @@ -50,10 +51,6 @@ export namespace Record { return Array.isArray(x) && 'label' in x; } - export function fallbackToString (_f: Value): string { - return ''; - } - export function constructorInfo, FieldsType extends Tuple>, T = GenericEmbedded>( r: Record): RecordConstructorInfo { @@ -83,19 +80,3 @@ export namespace Record { }; } } - -Array.prototype.asPreservesText = function (): string { - if ('label' in (this as any)) { - const r = this as Record, GenericEmbedded>; - return '<' + r.label.asPreservesText() + (r.length > 0 ? ' ': '') + - r.map(f => { - try { - return f.asPreservesText(); - } catch (e) { - return Record.fallbackToString(f); - } - }).join(' ') + '>'; - } else { - return '[' + this.map(i => i.asPreservesText()).join(', ') + ']'; - } -}; diff --git a/implementations/javascript/packages/core/src/runtime.ts b/implementations/javascript/packages/core/src/runtime.ts index ed9da23..4b9909e 100644 --- a/implementations/javascript/packages/core/src/runtime.ts +++ b/implementations/javascript/packages/core/src/runtime.ts @@ -18,3 +18,4 @@ export * from './record'; export * from './strip'; export * from './text'; export * from './values'; +export * from './writer'; diff --git a/implementations/javascript/packages/core/src/text.ts b/implementations/javascript/packages/core/src/text.ts index ae26596..2117137 100644 --- a/implementations/javascript/packages/core/src/text.ts +++ b/implementations/javascript/packages/core/src/text.ts @@ -1,15 +1,13 @@ +import type { GenericEmbedded } from './embedded'; import type { Value } from './values'; -export function stringify(x: any): string { - if (typeof x?.asPreservesText === 'function') { - return x.asPreservesText(); - } else { - try { - return JSON.stringify(x); - } catch (_e) { - return ('' + x).asPreservesText(); - } - } +import { Annotated } from './annotated'; +import { Bytes } from './bytes'; +import { KeyedDictionary, KeyedSet } from './dictionary'; +import { Writer, Writable, WriterOptions } from './writer'; + +export function stringify(x: any, options?: WriterOptions): string { + return Writer.stringify(x as Writable, options); } export function preserves(pieces: TemplateStringsArray, ...values: Value[]): string { @@ -21,32 +19,6 @@ export function preserves(pieces: TemplateStringsArray, ...values: Value[] return result.join(''); } - -declare global { - interface Object { asPreservesText(): string; } -} - -Object.defineProperty(Object.prototype, 'asPreservesText', { - enumerable: false, - writable: true, - value: function(): string { - return JSON.stringify(this); - } +[Annotated, Bytes, KeyedDictionary, KeyedSet].forEach((C) => { + C.prototype.toString = function () { return stringify(this); }; }); - -Boolean.prototype.asPreservesText = function (): string { - return this ? '#t' : '#f'; -}; - -Number.prototype.asPreservesText = function (): string { - return '' + this; -}; - -String.prototype.asPreservesText = function (): string { - return JSON.stringify(this); -}; - -Symbol.prototype.asPreservesText = function (): string { - // TODO: escaping - return this.description ?? '||'; -}; diff --git a/implementations/javascript/packages/core/src/writer.ts b/implementations/javascript/packages/core/src/writer.ts new file mode 100644 index 0000000..7de2eeb --- /dev/null +++ b/implementations/javascript/packages/core/src/writer.ts @@ -0,0 +1,328 @@ +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"; + +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 const genericEmbeddedTypeEncode: EmbeddedTypeEncode = { + encode(s: EncoderState, v: GenericEmbedded): void { + new Encoder(s, this).push(v.generic); + }, + + toValue(v: GenericEmbedded): Value { + return v.generic; + } +}; + +export const neverEmbeddedTypeEncode: EmbeddedTypeEncode = { + 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 encoding Preserves document"); + } +}; + +export interface WriterStateOptions { + includeAnnotations?: boolean; + indent?: number; + maxBinaryAsciiLength?: number; + maxBinaryAsciiProportion?: number; +} + +export interface WriterOptions extends WriterStateOptions { + embeddedEncode?: EmbeddedTypeEncode; +} + +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; + embeddedType: EmbeddedTypeEncode; + + constructor(state: WriterState, embeddedType: EmbeddedTypeEncode); + constructor(options?: WriterOptions); + constructor( + state_or_options: (WriterState | WriterOptions) = {}, + embeddedType?: EmbeddedTypeEncode + ) { + if (state_or_options instanceof WriterState) { + this.state = state_or_options; + this.embeddedType = embeddedType!; + } else { + this.state = new WriterState(state_or_options); + this.embeddedType = state_or_options.embeddedEncode ?? 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. + const m = /^[a-zA-Z~!$%^&*?_=+/.][-a-zA-Z~!$%^&*?_=+/.0-9]*$/.exec(s); + if (m) { + 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('#!'); + new Writer(this.state, genericEmbeddedTypeEncode) + .push(this.embeddedType.toValue(v.embeddedValue)); + })(v); + } + break; + default: + throw new Error(`Internal error: unhandled in Preserves Writer.push for ${v}`); + } + return this; // for chaining + } +} diff --git a/implementations/javascript/packages/schema/src/checker.ts b/implementations/javascript/packages/schema/src/checker.ts index 8c835aa..ef756dc 100644 --- a/implementations/javascript/packages/schema/src/checker.ts +++ b/implementations/javascript/packages/schema/src/checker.ts @@ -1,3 +1,4 @@ +import { stringify } from '@preserves/core'; import * as M from './meta'; export function checkSchema(schema: M.Schema): ( @@ -121,7 +122,7 @@ class Checker { this.checkNamedPattern( scope, M.promoteNamedSimplePattern(np), - `entry ${key.asPreservesText()} in dictionary in ${context}`)); + `entry ${stringify(key)} in dictionary in ${context}`)); break; } })(p.value); diff --git a/implementations/javascript/packages/schema/src/compiler/context.ts b/implementations/javascript/packages/schema/src/compiler/context.ts index 839ed1a..9324d97 100644 --- a/implementations/javascript/packages/schema/src/compiler/context.ts +++ b/implementations/javascript/packages/schema/src/compiler/context.ts @@ -1,4 +1,4 @@ -import { Dictionary, KeyedSet, FlexSet, Position, stringify, is } from "@preserves/core"; +import { Dictionary, KeyedSet, FlexSet, Position, stringify } from "@preserves/core"; import { refPosition } from "../reader"; import * as M from "../meta"; import { anglebrackets, block, braces, commas, formatItems, Item, keyvalue, seq, opseq } from "./block"; @@ -58,7 +58,7 @@ export class ModuleContext { literal(v: M.Input): Item { let varname = this.literals.get(v); if (varname === void 0) { - varname = M.jsId('$' + v.asPreservesText(), () => '__lit' + this.literals.size); + varname = M.jsId('$' + stringify(v), () => '__lit' + this.literals.size); this.literals.set(v, varname); } return varname;