Javascript pretty-printer
This commit is contained in:
parent
6cd8cb2c37
commit
0129901dab
|
@ -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<T = GenericEmbedded> implements Preservable<T> {
|
||||
export class Annotated<T = GenericEmbedded> implements Preservable<T>, PreserveWritable<T> {
|
||||
readonly annotations: Array<Value<T>>;
|
||||
readonly pos: Position | null;
|
||||
readonly item: Value<T>;
|
||||
|
@ -82,6 +81,22 @@ export class Annotated<T = GenericEmbedded> implements Preservable<T> {
|
|||
encoder.push(this.item);
|
||||
}
|
||||
|
||||
__preserve_text_on__(w: Writer<T>): 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<T = GenericEmbedded> implements Preservable<T> {
|
|||
// 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;
|
||||
}
|
||||
|
|
|
@ -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<never> {
|
||||
export class Bytes implements Preservable<any>, PreserveWritable<any> {
|
||||
readonly _view: Uint8Array;
|
||||
|
||||
constructor(maybeByteIterable: any = new Uint8Array()) {
|
||||
|
@ -122,10 +123,6 @@ export class Bytes implements Preservable<never> {
|
|||
return textDecoder.decode(this._view);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
__as_preserve__<T = GenericEmbedded>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
@ -134,26 +131,6 @@ export class Bytes implements Preservable<never> {
|
|||
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<never> {
|
|||
return this.toHex();
|
||||
}
|
||||
|
||||
__preserve_on__(encoder: Encoder<never>) {
|
||||
__preserve_on__(encoder: Encoder<any>) {
|
||||
encoder.state.emitbyte(Tag.ByteString);
|
||||
encoder.state.varint(this.length);
|
||||
encoder.state.emitbytes(this._view);
|
||||
}
|
||||
|
||||
__preserve_text_on__(w: Writer<any>) {
|
||||
w.state.writeBytes(this._view);
|
||||
}
|
||||
|
||||
get [IsPreservesBytes](): boolean {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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<K extends Value<T>, V, T = GenericEmbedded> extends FlexMap<K, V> implements Preservable<T> {
|
||||
export class KeyedDictionary<K extends Value<T>, V, T = GenericEmbedded> extends FlexMap<K, V>
|
||||
implements Preservable<T>, PreserveWritable<T>
|
||||
{
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Dictionary';
|
||||
}
|
||||
|
@ -34,21 +37,10 @@ export class KeyedDictionary<K extends Value<T>, V, T = GenericEmbedded> extends
|
|||
return result;
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '{' +
|
||||
Array.from(_iterMap(this.entries(), ([k, v]) =>
|
||||
stringify(k) + ': ' + stringify(v))).join(', ') +
|
||||
'}';
|
||||
}
|
||||
|
||||
clone(): KeyedDictionary<K, V, T> {
|
||||
return new KeyedDictionary(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() { return 'Dictionary'; }
|
||||
|
||||
__preserve_on__(encoder: Encoder<T>) {
|
||||
|
@ -72,6 +64,22 @@ export class KeyedDictionary<K extends Value<T>, V, T = GenericEmbedded> extends
|
|||
encoder.state.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
|
||||
__preserve_text_on__(w: Writer<T>) {
|
||||
w.state.writeSeq('{', '}', this.entries(), ([k, v]) => {
|
||||
w.push(k);
|
||||
if (Annotated.isAnnotated<T>(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<T>); // Suuuuuuuper unsound
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Dictionary<T = GenericEmbedded, V = Value<T>> extends KeyedDictionary<Value<T>, V, T> {
|
||||
|
@ -84,7 +92,9 @@ export class Dictionary<T = GenericEmbedded, V = Value<T>> extends KeyedDictiona
|
|||
}
|
||||
}
|
||||
|
||||
export class KeyedSet<K extends Value<T>, T = GenericEmbedded> extends FlexSet<K> implements Preservable<T> {
|
||||
export class KeyedSet<K extends Value<T>, T = GenericEmbedded> extends FlexSet<K>
|
||||
implements Preservable<T>, PreserveWritable<T>
|
||||
{
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Set';
|
||||
}
|
||||
|
@ -107,16 +117,6 @@ export class KeyedSet<K extends Value<T>, T = GenericEmbedded> extends FlexSet<K
|
|||
return result;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '#{' +
|
||||
Array.from(_iterMap(this.values(), stringify)).join(', ') +
|
||||
'}';
|
||||
}
|
||||
|
||||
clone(): KeyedSet<K, T> {
|
||||
return new KeyedSet(this);
|
||||
}
|
||||
|
@ -132,6 +132,10 @@ export class KeyedSet<K extends Value<T>, T = GenericEmbedded> extends FlexSet<K
|
|||
encoder.encodevalues(Tag.Set, this);
|
||||
}
|
||||
}
|
||||
|
||||
__preserve_text_on__(w: Writer<T>) {
|
||||
w.state.writeSeq('#{', '}', this, vv => w.push(vv));
|
||||
}
|
||||
}
|
||||
|
||||
export class Set<T = GenericEmbedded> extends KeyedSet<Value<T>, T> {
|
||||
|
|
|
@ -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<T> {
|
|||
return isEmbedded<T>(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__<R>(): T extends R ? Value<R> : 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<GenericEmbedded> = {
|
||||
encode(s: EncoderState, v: GenericEmbedded): void {
|
||||
new Encoder(s, this).push(v.generic);
|
||||
},
|
||||
|
||||
toValue(v: GenericEmbedded): Value<GenericEmbedded> {
|
||||
return v.generic;
|
||||
}
|
||||
};
|
||||
import type { GenericEmbedded, EmbeddedType, EmbeddedTypeDecode } from "./embedded";
|
||||
|
||||
export const genericEmbeddedType: EmbeddedType<GenericEmbedded> =
|
||||
Object.assign({},
|
||||
genericEmbeddedTypeDecode,
|
||||
genericEmbeddedTypeEncode);
|
||||
|
||||
export const neverEmbeddedTypeEncode: EmbeddedTypeEncode<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 encoding Preserves document");
|
||||
}
|
||||
};
|
||||
|
||||
export const neverEmbeddedType: EmbeddedType<never> =
|
||||
Object.assign({},
|
||||
neverEmbeddedTypeDecode,
|
||||
|
|
|
@ -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<T> =
|
||||
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
||||
|
@ -248,15 +249,14 @@ export class Encoder<T = object> {
|
|||
for (let i of v) { this.push(i); }
|
||||
this.state.emitbyte(Tag.End);
|
||||
}
|
||||
else if (Array.isArray(v)) {
|
||||
else if (isIterable<Value<T>>(v)) {
|
||||
this.encodevalues(Tag.Sequence, v);
|
||||
}
|
||||
else if (isIterable<Value<T>>(v)) {
|
||||
this.encodevalues(Tag.Sequence, v as Iterable<Value<T>>);
|
||||
}
|
||||
else {
|
||||
this.state.emitbyte(Tag.Embedded);
|
||||
this.embeddedEncode.encode(this.state, v.embeddedValue);
|
||||
((v: Embedded<T>) => {
|
||||
this.state.emitbyte(Tag.Embedded);
|
||||
this.embeddedEncode.encode(this.state, v.embeddedValue);
|
||||
})(v);
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
|
|
|
@ -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<any> {
|
||||
export class SingleFloat extends Float implements Preservable<any>, PreserveWritable<any> {
|
||||
__as_preserve__<T = GenericEmbedded>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
@ -59,12 +60,12 @@ export class SingleFloat extends Float implements Preservable<any> {
|
|||
encoder.state.index += 4;
|
||||
}
|
||||
|
||||
get [FloatType](): 'Single' {
|
||||
return 'Single';
|
||||
__preserve_text_on__(w: Writer<any>) {
|
||||
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<any> {
|
||||
export class DoubleFloat extends Float implements Preservable<any>, PreserveWritable<any> {
|
||||
__as_preserve__<T = GenericEmbedded>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
@ -88,12 +89,12 @@ export class DoubleFloat extends Float implements Preservable<any> {
|
|||
encoder.state.index += 8;
|
||||
}
|
||||
|
||||
get [FloatType](): 'Double' {
|
||||
return 'Double';
|
||||
__preserve_text_on__(w: Writer<any>) {
|
||||
w.state.pieces.push('' + this.value);
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '' + this.value;
|
||||
get [FloatType](): 'Double' {
|
||||
return 'Double';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GenericEmbedded } from "./embedded";
|
||||
import { is } from "./is";
|
||||
import { Value } from "./values";
|
||||
import { Writer } from "./writer";
|
||||
|
||||
export type Tuple<T> = Array<T> | [T];
|
||||
|
||||
|
@ -50,10 +51,6 @@ export namespace Record {
|
|||
return Array.isArray(x) && 'label' in x;
|
||||
}
|
||||
|
||||
export function fallbackToString (_f: Value<any>): string {
|
||||
return '<unprintable_preserves_field_value>';
|
||||
}
|
||||
|
||||
export function constructorInfo<L extends Value<T>, FieldsType extends Tuple<Value<T>>, T = GenericEmbedded>(
|
||||
r: Record<L, FieldsType, T>): RecordConstructorInfo<L, T>
|
||||
{
|
||||
|
@ -83,19 +80,3 @@ export namespace Record {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.asPreservesText = function (): string {
|
||||
if ('label' in (this as any)) {
|
||||
const r = this as Record<Value, Tuple<Value>, 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(', ') + ']';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,3 +18,4 @@ export * from './record';
|
|||
export * from './strip';
|
||||
export * from './text';
|
||||
export * from './values';
|
||||
export * from './writer';
|
||||
|
|
|
@ -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<T = GenericEmbedded>(x: any, options?: WriterOptions<T>): string {
|
||||
return Writer.stringify(x as Writable<T>, options);
|
||||
}
|
||||
|
||||
export function preserves<T>(pieces: TemplateStringsArray, ...values: Value<T>[]): string {
|
||||
|
@ -21,32 +19,6 @@ export function preserves<T>(pieces: TemplateStringsArray, ...values: Value<T>[]
|
|||
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 ?? '||';
|
||||
};
|
||||
|
|
|
@ -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<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 const genericEmbeddedTypeEncode: EmbeddedTypeEncode<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> = {
|
||||
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 encoding Preserves document");
|
||||
}
|
||||
};
|
||||
|
||||
export interface WriterStateOptions {
|
||||
includeAnnotations?: boolean;
|
||||
indent?: number;
|
||||
maxBinaryAsciiLength?: number;
|
||||
maxBinaryAsciiProportion?: number;
|
||||
}
|
||||
|
||||
export interface WriterOptions<T> extends WriterStateOptions {
|
||||
embeddedEncode?: EmbeddedTypeEncode<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;
|
||||
embeddedType: EmbeddedTypeEncode<T>;
|
||||
|
||||
constructor(state: WriterState, embeddedType: EmbeddedTypeEncode<T>);
|
||||
constructor(options?: WriterOptions<T>);
|
||||
constructor(
|
||||
state_or_options: (WriterState | WriterOptions<T>) = {},
|
||||
embeddedType?: EmbeddedTypeEncode<T>
|
||||
) {
|
||||
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<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.
|
||||
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<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('#!');
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue