# HG changeset patch # User cin # Date 2019-06-24 05:42:32 # Node ID 025f02eff3b2adfb99a64bcc38ae57c112723b03 # Parent 2ac7406b138e4f827eb4e7185de226baf0b17d81 StringBuilder, TextWriter, ConsoleWriter tests diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,8 @@ def createSoursetTasks = { String name, '-t', target, '-m', jsmodule, '-d', + '--sourceMap', + '--sourceRoot', "file://$setDir/ts", '--outDir', compileDir, '--declarationDir', declDir diff --git a/gradle.properties b/gradle.properties --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ description=Dependency injection, loggin license=BSD-2-Clause repository=https://bitbucket.org/implab/implabjs-core npmScope=implab -npmName=core \ No newline at end of file +npmName=core-amd \ No newline at end of file diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -144,7 +144,7 @@ "dependencies": { "tape": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.3.tgz", + "resolved": "http://registry.npmjs.org/tape/-/tape-2.3.3.tgz", "integrity": "sha1-Lnzgox3wn41oUWZKcYQuDKUFevc=", "dev": true, "requires": { @@ -277,7 +277,7 @@ }, "minimist": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", "dev": true }, @@ -316,7 +316,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -369,7 +369,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, @@ -432,7 +432,7 @@ }, "through2": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz", "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { diff --git a/src/amd/ts/text/TemplateCompiler.ts b/src/amd/ts/text/TemplateCompiler.ts --- a/src/amd/ts/text/TemplateCompiler.ts +++ b/src/amd/ts/text/TemplateCompiler.ts @@ -7,6 +7,23 @@ const trace = TraceSource.get(m.id); type TemplateFn = (obj: object) => string; +const htmlEscapes = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/" +}; + +// Regex containing the keys listed immediately above. +const htmlEscaper = /[&<>"'\/]/g; + +// Escape a string for HTML interpolation. +function escapeHtml(string: any) { + return ("" + string).replace(htmlEscaper, match => htmlEscapes[match]); +} + export class TemplateCompiler { _data: string[]; @@ -27,14 +44,14 @@ export class TemplateCompiler { try { // tslint:disable-next-line:function-constructor - const compiled = new Function("obj, format, $data", text); + const compiled = new Function("obj, format, $data, escapeHtml", text); /** * Функция форматирования по шаблону * * @type{Function} * @param{Object} obj объект с параметрами для подстановки */ - return (obj: object) => compiled(obj || {}, format, this._data); + return (obj: object) => compiled(obj || {}, format, this._data, escapeHtml); } catch (e) { trace.traceEvent(DebugLevel, [e, text, this._data]); throw e; @@ -69,6 +86,9 @@ export class TemplateCompiler { case TokenType.OpenInlineBlock: this.visitInline(parser); break; + case TokenType.OpenFilterBlock: + this.visitFilter(parser); + break; default: this.visitTextFragment(parser); break; @@ -87,6 +107,17 @@ export class TemplateCompiler { this._code.push(code.join("")); } + visitFilter(parser: ITemplateParser) { + const code = ["$p.push(escapeHtml("]; + while (parser.next()) { + if (parser.token() === TokenType.CloseBlock) + break; + code.push(parser.value()); + } + code.push("));"); + this._code.push(code.join("")); + } + visitCode(parser: ITemplateParser) { const code = []; while (parser.next()) { diff --git a/src/amd/ts/text/TemplateParser.ts b/src/amd/ts/text/TemplateParser.ts --- a/src/amd/ts/text/TemplateParser.ts +++ b/src/amd/ts/text/TemplateParser.ts @@ -5,12 +5,13 @@ import m = require("module"); const trace = TraceSource.get(m.id); -const splitRx = /(<%=|\[%=|<%|\[%|%\]|%>)/; +const splitRx = /(<%=|<%~|\[%~|\[%=|<%|\[%|%\]|%>)/; export enum TokenType { None, Text, OpenInlineBlock, + OpenFilterBlock, OpenBlock, CloseBlock } @@ -20,6 +21,8 @@ const tokenMap: MapOf = { "[%": TokenType.OpenBlock, "<%=": TokenType.OpenInlineBlock, "[%=": TokenType.OpenInlineBlock, + "<%~": TokenType.OpenFilterBlock, + "[%~": TokenType.OpenFilterBlock, "%>": TokenType.CloseBlock, "%]": TokenType.CloseBlock }; diff --git a/src/main/ts/interfaces.ts b/src/main/ts/interfaces.ts --- a/src/main/ts/interfaces.ts +++ b/src/main/ts/interfaces.ts @@ -101,11 +101,11 @@ export interface IObserver { } export interface TextWriter { - Write(obj: any): void; - Write(format: string, ...args: any[]): void; + write(obj: any): void; + write(format: string, ...args: any[]): void; - WriteLine(obj: any): void; - WriteLine(format: string, ...args: any[]): void; + writeLine(obj?: any): void; + writeLine(format: string, ...args: any[]): void; - WriteValue(value: any, spec?: string): void; + writeValue(value: any, spec?: string): void; } diff --git a/src/main/ts/log/ConsoleWriter.ts b/src/main/ts/log/ConsoleWriter.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/log/ConsoleWriter.ts @@ -0,0 +1,100 @@ +import { TextWriterBase } from "../text/TextWriterBase"; +import { isNull, isNullOrEmptyString, isPrimitive } from "../safe"; +import { NullConsole } from "./NullConsole"; + +interface LogConsole { + debug(...args: any[]): void; + log(...args: any[]): void; + warn(...args: any[]): void; + error(...args: any[]): void; +} + +function hasConsole() { + try { + // tslint:disable-next-line:no-console + return (typeof console !== "undefined" && typeof console.log === "function"); + } catch { + return false; + } +} + +function getConsole() { + return hasConsole() ? console : NullConsole.instance; +} + +export class ConsoleWriter extends TextWriterBase { + static readonly default = new ConsoleWriter(getConsole()); + + private _buffer: any[]; + + private _out: LogConsole; + private _level: keyof LogConsole; + + constructor(out?: LogConsole) { + super(); + this._out = out || NullConsole.instance; + this._buffer = []; + this._level = "log"; + } + + getLogLevel() { + return this._level; + } + + setLogLevel(level: keyof LogConsole) { + this._level = level; + } + + /** Flushes the buffer to the console + */ + writeNewLine() { + // group text chunks together, and let objects as is + // ['a', 'b', {foo: 'bar'}, 'c', 'd'] -> ['ab', {foo: 'bar'}, 'cd'] + // this will prevent from additional spaces to occur in the console + // ['a', 'b'] will be printed as 'a b' rather then 'ab'. + + // console.log("writeLine", this._buffer); + + let offset = 0; + const args = []; + this._buffer.forEach((v, i) => { + if (!isPrimitive(v)) { + if (offset < i) + args.push(i - offset > 1 ? this._buffer.slice(offset, i).join("") : this._buffer[offset]); + args.push(v); + offset = i + 1; + } + }); + if (offset < this._buffer.length) + args.push(this._buffer.slice(offset).join("")); + + // console.log("WriteLine", args); + + this._out[this._level].apply(this._out, args); + + this._buffer = []; + } + + /** Adds a text chunk to the buffer. Buffer contents will be flushed when + * the end of line will be printed. + * + * @param text The text to be added to the buffer. + */ + writeText(text: string) { + this._buffer.push(text); + } + + /** Wrotes the specified value to the buffer. + * + * @param value The value to be added to the buffer + * @param spec The instructions how to format the value, is this parameter + * is ommited the raw value will be added to the buffer and + * passed directly to the console out. + */ + writeValue(value: any, spec?: string) { + if (isNullOrEmptyString(spec)) + this._buffer.push(value); + else + super.writeValue(value); + } +} diff --git a/src/main/ts/log/NullConsole.ts b/src/main/ts/log/NullConsole.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/log/NullConsole.ts @@ -0,0 +1,52 @@ +export class NullConsole { + static readonly instance = new NullConsole(); + + assert(condition?: boolean, message?: string, ...data: any[]): void { + } + clear(): void { + } + count(label?: string): void { + } + debug(message?: any, ...optionalParams: any[]): void { + } + dir(value?: any, ...optionalParams: any[]): void { + } + dirxml(value: any): void { + } + error(message?: any, ...optionalParams: any[]): void { + } + exception(message?: string, ...optionalParams: any[]): void { + } + group(groupTitle?: string, ...optionalParams: any[]): void { + } + groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void { + } + groupEnd(): void { + } + info(message?: any, ...optionalParams: any[]): void { + } + log(message?: any, ...optionalParams: any[]): void { + } + markTimeline(label?: string): void { + } + profile(reportName?: string): void { + } + profileEnd(reportName?: string): void { + } + table(...tabularData: any[]): void { + } + time(label?: string): void { + } + timeEnd(label?: string): void { + } + timeStamp(label?: string): void { + } + timeline(label?: string): void { + } + timelineEnd(label?: string): void { + } + trace(message?: any, ...optionalParams: any[]): void { + } + warn(message?: any, ...optionalParams: any[]): void { + } +} diff --git a/src/main/ts/log/TraceEventData.ts b/src/main/ts/log/TraceEventData.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/log/TraceEventData.ts @@ -0,0 +1,20 @@ +import { TraceEvent, TraceSource } from "./TraceSource"; +import { format } from "../text/StringBuilder"; + +export class TraceEventData implements TraceEvent { + source: TraceSource; + level: number; + message: any; + args?: any[]; + + constructor(source: TraceSource, level: number, message: any, args: any[]) { + this.source = source; + this.level = level; + this.message = message; + this.args = args; + } + + toString() { + return format(this.message, ...this.args); + } +} diff --git a/src/main/ts/log/TraceSource.ts b/src/main/ts/log/TraceSource.ts --- a/src/main/ts/log/TraceSource.ts +++ b/src/main/ts/log/TraceSource.ts @@ -1,6 +1,6 @@ import { Observable } from "../Observable"; import { Registry } from "./Registry"; -import { format as _format } from "../text/StringFormat"; +import { TraceEventData } from "./TraceEventData"; export const DebugLevel = 400; @@ -17,13 +17,9 @@ export interface TraceEvent { readonly level: number; - readonly arg: any; -} + readonly message: any; -function format(msg) { - if (typeof(msg) !== "string" || arguments.length === 1) - return msg; - return _format.apply(null, arguments); + readonly args?: any[]; } export class TraceSource { @@ -43,35 +39,41 @@ export class TraceSource { }); } - protected emit(level: number, arg: any) { - this._notifyNext({ source: this, level, arg }); + protected emit(level: number, message: any, args?: any[]) { + this._notifyNext(new TraceEventData(this, level, message, args)); } isDebugEnabled() { return this.level >= DebugLevel; } + debug(data: any): void; + debug(msg: string, ...args: any[]): void; debug(msg: string, ...args: any[]) { if (this.isEnabled(DebugLevel)) - this.emit(DebugLevel, format.apply(null, arguments)); + this.emit(DebugLevel, msg, args); } isLogEnabled() { return this.level >= LogLevel; } + log(data: any): void; + log(msg: string, ...args: any[]): void; log(msg: string, ...args: any[]) { if (this.isEnabled(LogLevel)) - this.emit(LogLevel, format.apply(null, arguments)); + this.emit(LogLevel, msg, args); } isWarnEnabled() { return this.level >= WarnLevel; } + warn(data: any): void; + warn(msg: string, ...args: any[]): void; warn(msg: string, ...args: any[]) { if (this.isEnabled(WarnLevel)) - this.emit(WarnLevel, format.apply(null, arguments)); + this.emit(WarnLevel, msg, args); } /** @@ -81,15 +83,20 @@ export class TraceSource { return this.level >= ErrorLevel; } + /** Traces a error + * @param data The object which will be passed to the underlying listeners + */ + error(data: any): void; /** * Traces a error. * * @param msg the message. * @param args parameters which will be substituted in the message. */ + error(msg: string, ...args: any[]): void; error(msg: string, ...args: any[]) { if (this.isEnabled(ErrorLevel)) - this.emit(ErrorLevel, format.apply(null, arguments)); + this.emit(ErrorLevel, msg, args); } /** @@ -106,11 +113,11 @@ export class TraceSource { * Traces a raw event, passing data as it is to the underlying listeners * * @param level the level of the event - * @param arg the data of the event, can be a simple string or any object. + * @param msg the data of the event, can be a simple string or any object. */ - traceEvent(level: number, arg: any) { + traceEvent(level: number, msg: any, ...args: any[]) { if (this.isEnabled(level)) - this.emit(level, arg); + this.emit(level, msg, args); } /** diff --git a/src/main/ts/log/writers/ConsoleLogger.ts b/src/main/ts/log/writers/ConsoleLogger.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/log/writers/ConsoleLogger.ts @@ -0,0 +1,41 @@ +import { IObservable, IDestroyable, ICancellation } from "../../interfaces"; +import { TraceEvent, LogLevel, WarnLevel, DebugLevel } from "../TraceSource"; +import { Cancellation } from "../../Cancellation"; +import { destroy, argumentNotNull } from "../../safe"; +import { ConsoleWriter } from "../ConsoleWriter"; + +export class ConsoleLogger implements IDestroyable { + private readonly _subscriptions = new Array(); + private readonly _writer: ConsoleWriter; + + constructor(writer = ConsoleWriter.default) { + argumentNotNull(writer, "writer"); + this._writer = writer; + } + + writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { + const subscription = source.on(this.writeEvent.bind(this)); + if (ct.isSupported()) { + ct.register(subscription.destroy.bind(subscription)); + } + this._subscriptions.push(subscription); + } + + writeEvent(next: TraceEvent) { + if (next.level >= DebugLevel) { + this._writer.setLogLevel("debug"); + } else if (next.level >= LogLevel) { + this._writer.setLogLevel("log"); + } else if (next.level >= WarnLevel) { + this._writer.setLogLevel("warn"); + } else { + this._writer.setLogLevel("error"); + } + this._writer.write("{0}: ", next.source.id); + this._writer.writeLine(next.message, ...next.args); + } + + destroy() { + this._subscriptions.forEach(destroy); + } +} diff --git a/src/main/ts/log/writers/ConsoleWriter.ts b/src/main/ts/log/writers/ConsoleWriter.ts --- a/src/main/ts/log/writers/ConsoleWriter.ts +++ b/src/main/ts/log/writers/ConsoleWriter.ts @@ -1,49 +1,1 @@ -import { IObservable, IDestroyable, ICancellation } from "../../interfaces"; -import { TraceEvent, LogLevel, WarnLevel, DebugLevel } from "../TraceSource"; -import { Cancellation } from "../../Cancellation"; -import { destroy } from "../../safe"; - -function hasConsole() { - try { - // tslint:disable-next-line:no-console - return (typeof console !== "undefined" && typeof console.log === "function"); - } catch { - return false; - } -} - -export class ConsoleWriter implements IDestroyable { - readonly _subscriptions = new Array(); - - writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { - const subscription = source.on(this.writeEvent.bind(this)); - if (ct.isSupported()) { - ct.register(subscription.destroy.bind(subscription)); - } - this._subscriptions.push(subscription); - } - - writeEvent(next: TraceEvent) { - // IE will create console only when devepoler tools are activated - if (!hasConsole()) - return; - - if (next.level >= DebugLevel) { - // tslint:disable-next-line:no-console - console.debug(next.source.id.toString(), next.arg); - } else if (next.level >= LogLevel) { - // tslint:disable-next-line:no-console - console.log(next.source.id.toString(), next.arg); - } else if (next.level >= WarnLevel) { - // tslint:disable-next-line:no-console - console.warn(next.source.id.toString(), next.arg); - } else { - // tslint:disable-next-line:no-console - console.error(next.source.id.toString(), next.arg); - } - } - - destroy() { - this._subscriptions.forEach(destroy); - } -} +export { ConsoleLogger as ConsoleWriter } from "./ConsoleLogger"; \ No newline at end of file diff --git a/src/main/ts/text/Converter.ts b/src/main/ts/text/Converter.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/text/Converter.ts @@ -0,0 +1,30 @@ +import { isPrimitive, isNull } from "../safe"; + +export class Converter { + static readonly default = new Converter(); + + convert(value: any, pattern: string) { + if (pattern && pattern.toLocaleLowerCase() === "json") { + const seen = []; + return JSON.stringify(value, (k, v) => { + if (!isPrimitive(v)) { + const id = seen.indexOf(v); + if (id >= 0) + return "@ref-" + id; + else { + seen.push(v); + return v; + } + } else { + return v; + } + }, 2); + } else if (isNull(value)) { + return ""; + } else if (value instanceof Date) { + return value.toISOString(); + } else { + return value.toString(); + } + } +} diff --git a/src/main/ts/text/FormatCompiler.ts b/src/main/ts/text/FormatCompiler.ts --- a/src/main/ts/text/FormatCompiler.ts +++ b/src/main/ts/text/FormatCompiler.ts @@ -1,20 +1,41 @@ import { FormatScanner, TokeType } from "./FormatScanner"; -import { isNullOrEmptyString } from "../safe"; -import { TextWriter } from "../interfaces"; +import { isNullOrEmptyString, isPrimitive, get } from "../safe"; +import { TextWriter, MapOf } from "../interfaces"; + +type CompiledPattern = (writer: TextWriter, args: any) => void; export class FormatCompiler { _scanner: FormatScanner; + _cache: MapOf = {}; - _parts: []; + _parts: Array; + + compile(pattern: string) { + let compiledPattern = this._cache && this._cache[pattern]; + if (!compiledPattern) { + this._scanner = new FormatScanner(pattern); + this._parts = []; + + this.visitText(); + const parts = this._parts; - compile() { - return (writer: TextWriter, args: any) => { - this._parts.forEach(x => writer.WriteValue(x)) - }; + compiledPattern = (writer: TextWriter, args: any) => { + parts.forEach(x => { + if (isPrimitive(x)) + writer.writeValue(x); + else + writer.writeValue(get(x.name, args), x.format); + }); + }; + if (this._cache) + this._cache[pattern] = compiledPattern; + } + return compiledPattern; } visitText() { while (this._scanner.next()) { + // console.log(this._scanner.getTokenType(), this._scanner.getTokenValue()); switch (this._scanner.getTokenType()) { case TokeType.CurlOpen: this.visitCurlOpen(); @@ -25,8 +46,6 @@ export class FormatCompiler { default: this.pushText(this._scanner.getTokenValue()); } - if (this._scanner.getTokenType() === TokeType.CurlOpen) - this.visitCurlOpen(); } } @@ -39,12 +58,14 @@ export class FormatCompiler { } visitCurlOpen() { - if (this._scanner.next()) { - if (this._scanner.getTokenType() === TokeType.CurlOpen) - this.pushText("{"); - else - this.visitTemplateSubst(); - } + if (!this._scanner.next()) + this.dieUnexpectedEnd("{ | TEXT"); + + if (this._scanner.getTokenType() === TokeType.CurlOpen) + this.pushText("{"); + else + this.visitTemplateSubst(); + } visitTemplateSubst() { @@ -52,20 +73,23 @@ export class FormatCompiler { this.dieUnexpectedToken("TEXT"); const fieldName = this._scanner.getTokenValue(); - const filedFormat = this.readColon() && this.readFieldFormat(); + const filedFormat = this.readColon() ? this.readFieldFormat() : null; + + if (this._scanner.getTokenType() !== TokeType.CurlClose) + this.dieUnexpectedToken("}"); this.pushSubst(fieldName, filedFormat); } readFieldFormat() { const parts = new Array(); - while (this._scanner.next()) { + do { if (this._scanner.getTokenType() === TokeType.CurlClose) { return parts.join(""); } else { parts.push(this._scanner.getTokenValue()); } - } + } while (this._scanner.next()); this.dieUnexpectedEnd("}"); } @@ -81,11 +105,12 @@ export class FormatCompiler { } pushSubst(fieldName: string, filedFormat: string) { - + // console.log("pushSubst ", fieldName, filedFormat); + this._parts.push({ name: fieldName, format: filedFormat }); } pushText(text: string) { - + this._parts.push(text); } dieUnexpectedToken(expected?: string) { diff --git a/src/main/ts/text/FormatScanner.ts b/src/main/ts/text/FormatScanner.ts --- a/src/main/ts/text/FormatScanner.ts +++ b/src/main/ts/text/FormatScanner.ts @@ -2,10 +2,10 @@ import { argumentNotEmptyString } from " import { MapOf } from "../interfaces"; export const enum TokeType { - CurlOpen, - CurlClose, - Colon, - Text + CurlOpen = 1, + CurlClose = 2, + Colon = 3, + Text = 4 } const typeMap = { @@ -16,7 +16,6 @@ const typeMap = { export class FormatScanner { private _text: string; - private _pos: number; private _tokenType: TokeType; private _tokenValue: string; private _rx = /[^{}:]+|(.)/g; @@ -29,7 +28,6 @@ export class FormatScanner { next() { if (this._rx.lastIndex >= this._text.length) return false; - this._pos = this._rx.lastIndex; const match = this._rx.exec(this._text); this._tokenType = typeMap[match[1]] || TokeType.Text; diff --git a/src/main/ts/text/NullTextWriter.ts b/src/main/ts/text/NullTextWriter.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/text/NullTextWriter.ts @@ -0,0 +1,19 @@ +import { TextWriter } from "../interfaces"; + +export class NullTextWriter implements TextWriter { + static readonly instance = new NullTextWriter(); + + write(obj: any): void; + write(format: string, ...args: any[]): void; + write() { + } + + writeLine(obj: any): void; + writeLine(format: string, ...args: any[]): void; + writeLine() { + } + + writeValue(value: any, spec?: string): void; + writeValue() { + } +} diff --git a/src/main/ts/text/StringBuilder.ts b/src/main/ts/text/StringBuilder.ts --- a/src/main/ts/text/StringBuilder.ts +++ b/src/main/ts/text/StringBuilder.ts @@ -1,18 +1,31 @@ -export class StringBuilder { - private _data: string[]; - private _newLine = "\n"; +import { TextWriterBase } from "./TextWriterBase"; +import { Converter } from "./Converter"; + +export class StringBuilder extends TextWriterBase { + private _data = new Array(); - Write(obj: any); - Write(format: string, ...args: any[]) { + constructor(converter = Converter.default) { + super(converter); + } + writeText(text: string) { + this._data.push(text); } - WriteLine(obj: any); - WriteLine(format: string, ...args: any[]) { - + toString() { + return this._data.join(""); } - WriteValue(value: any, spec?: string) { - + clear() { + this._data.length = 0; } } + +const sb = new StringBuilder(); + +export function format(format: string, ...args: any): string; +export function format() { + sb.clear(); + sb.write.apply(sb, arguments); + return sb.toString(); +} diff --git a/src/main/ts/text/TextWriterBase.ts b/src/main/ts/text/TextWriterBase.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/text/TextWriterBase.ts @@ -0,0 +1,48 @@ +import { TextWriter } from "../interfaces"; +import { FormatCompiler } from "./FormatCompiler"; +import { isString, argumentNotNull } from "../safe"; +import { Converter } from "./Converter"; + +const compiler = new FormatCompiler(); + +export abstract class TextWriterBase implements TextWriter { + private _converter: Converter; + + constructor(converter = Converter.default) { + argumentNotNull(converter, "converter"); + this._converter = converter; + } + + writeNewLine() { + this.writeValue("\n"); + } + + write(obj: any): void; + write(format: string, ...args: any[]): void; + write(format: any, ...args: any[]): void { + if (args.length) { + const compiled = compiler.compile(format); + compiled(this, args); + } else { + this.writeValue(format); + } + } + + writeLine(obj?: any): void; + writeLine(format: string, ...args: any[]): void; + writeLine(): void { + if (arguments.length) + this.write.apply(this, arguments); + this.writeNewLine(); + } + + writeValue(value: any, spec?: string): void { + this.writeText( + isString(value) ? + value : + this._converter.convert(value, spec) + ); + } + + abstract writeText(text: string); +} diff --git a/src/test/ts/ActivatableTests.ts b/src/test/ts/ActivatableTests.ts --- a/src/test/ts/ActivatableTests.ts +++ b/src/test/ts/ActivatableTests.ts @@ -1,8 +1,8 @@ -import * as tape from "tape"; import { MockActivationController } from "./mock/MockActivationController"; import { SimpleActivatable } from "./mock/SimpleActivatable"; +import { test } from "./TestTraits"; -tape("simple activation", async t => { +test("simple activation", async t => { const a = new SimpleActivatable(); t.false(a.isActive()); @@ -12,11 +12,9 @@ tape("simple activation", async t => { await a.deactivate(); t.false(a.isActive()); - - t.end(); }); -tape("controller activation", async t => { +test("controller activation", async t => { const a = new SimpleActivatable(); const c = new MockActivationController(); @@ -37,11 +35,9 @@ tape("controller activation", async t => t.false(a.isActive(), "The component should successfully deactivate"); t.equal(c.getActive(), null, "The controller shouldn't point to any component"); t.equal(a.getActivationController(), c, "The componet should point to it's controller"); - - t.end(); }); -tape("handle error in onActivating", async t => { +test("handle error in onActivating", async t => { const a = new SimpleActivatable(); a.onActivating = async () => { @@ -55,6 +51,4 @@ tape("handle error in onActivating", asy } t.false(a.isActive(), "the component should remain inactive"); - - t.end(); }); diff --git a/src/test/ts/CancellationTests.ts b/src/test/ts/CancellationTests.ts --- a/src/test/ts/CancellationTests.ts +++ b/src/test/ts/CancellationTests.ts @@ -1,8 +1,8 @@ -import * as tape from "tape"; import { Cancellation } from "@implab/core/Cancellation"; import { delay } from "@implab/core/safe"; +import { test } from "./TestTraits"; -tape("standalone cancellation", async t => { +test("standalone cancellation", async t => { let doCancel: (e) => void; @@ -43,11 +43,9 @@ tape("standalone cancellation", async t msg = e; } t.equals(msg, reason, "The cancellation reason should be catched"); - - t.end(); }); -tape("async cancellation", async t => { +test("async cancellation", async t => { const ct = new Cancellation(cancel => { cancel("STOP!"); @@ -59,11 +57,9 @@ tape("async cancellation", async t => { } catch (e) { t.equals(e, "STOP!", "Should throw the cancellation reason"); } - - t.end(); }); -tape("cancel with external event", async t => { +test("cancel with external event", async t => { const ct = new Cancellation(cancel => { setTimeout(x => cancel("STOP!"), 0); }); @@ -74,11 +70,9 @@ tape("cancel with external event", async } catch (e) { t.equals(e, "STOP!", "Should throw the cancellation reason"); } - - t.end(); }); -tape("operation normal flow", async t => { +test("operation normal flow", async t => { let htimeout; const ct = new Cancellation(cancel => { @@ -91,6 +85,4 @@ tape("operation normal flow", async t => } finally { clearTimeout(htimeout); } - - t.end(); }); diff --git a/src/test/ts/ObservableTests.ts b/src/test/ts/ObservableTests.ts --- a/src/test/ts/ObservableTests.ts +++ b/src/test/ts/ObservableTests.ts @@ -1,12 +1,12 @@ import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource"; -import * as tape from "tape"; import { Observable } from "@implab/core/Observable"; import { IObservable } from "@implab/core/interfaces"; import { delay } from "@implab/core/safe"; +import { test } from "./TestTraits"; const trace = TraceSource.get("ObservableTests"); -tape("events sequence example", async t => { +test("events sequence example", async t => { let events: IObservable; @@ -34,11 +34,9 @@ tape("events sequence example", async t t.equals(count, 45, "the summ of the evetns"); t.true(complete, "the sequence is complete"); - - t.end(); }); -tape("event sequence termination", async t => { +test("event sequence termination", async t => { let events: IObservable; const done = new Promise(resolve => { @@ -68,6 +66,4 @@ tape("event sequence termination", async await done; t.equals(count, 1, "the sequence must be terminated once"); - - t.end(); }); diff --git a/src/test/ts/SafeTests.ts b/src/test/ts/SafeTests.ts --- a/src/test/ts/SafeTests.ts +++ b/src/test/ts/SafeTests.ts @@ -1,8 +1,8 @@ -import tape = require("tape"); import { Cancellation } from "@implab/core/Cancellation"; import { first, isPromise, firstWhere, delay, nowait } from "@implab/core/safe"; +import { test } from "./TestTraits"; -tape("await delay test", async t => { +test("await delay test", async t => { // schedule delay let resolved = false; let res = delay(0).then(() => resolved = true); @@ -36,11 +36,9 @@ tape("await delay test", async t => { // try schedule delay after the cancellation is requested nowait(delay(0, ct)); }, "Should throw if cancelled before start"); - - t.end(); }); -tape("sequemce test", async t => { +test("sequemce test", async t => { const sequence = ["a", "b", "c"]; const empty = []; @@ -94,6 +92,4 @@ tape("sequemce test", async t => { v = await new Promise(resolve => first(asyncSequence, resolve)); t.equal(v, "a", "The callback should be called for the first element"); - - t.end(); }); diff --git a/src/test/ts/TestTraits.ts b/src/test/ts/TestTraits.ts --- a/src/test/ts/TestTraits.ts +++ b/src/test/ts/TestTraits.ts @@ -5,9 +5,10 @@ import * as tape from "tape"; import { argumentNotNull, destroy } from "@implab/core/safe"; export class TapeWriter implements IDestroyable { - readonly _tape: tape.Test; + private readonly _tape: tape.Test; - _subscriptions = new Array(); + private readonly _subscriptions = new Array(); + private _destroyed; constructor(t: tape.Test) { argumentNotNull(t, "tape"); @@ -15,22 +16,24 @@ export class TapeWriter implements IDest } writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { - const subscription = source.on(this.writeEvent.bind(this)); - if (ct.isSupported()) { - ct.register(subscription.destroy.bind(subscription)); + if (!this._destroyed) { + const subscription = source.on(this.writeEvent.bind(this)); + if (ct.isSupported()) { + ct.register(subscription.destroy.bind(subscription)); + } + this._subscriptions.push(subscription); } - this._subscriptions.push(subscription); } writeEvent(next: TraceEvent) { if (next.level >= DebugLevel) { - this._tape.comment(`DEBUG ${next.source.id} ${next.arg}`); + this._tape.comment(`DEBUG ${next.source.id} ${next}`); } else if (next.level >= LogLevel) { - this._tape.comment(`LOG ${next.source.id} ${next.arg}`); + this._tape.comment(`LOG ${next.source.id} ${next}`); } else if (next.level >= WarnLevel) { - this._tape.comment(`WARN ${next.source.id} ${next.arg}`); + this._tape.comment(`WARN ${next.source.id} ${next}`); } else { - this._tape.comment(`ERROR ${next.source.id} ${next.arg}`); + this._tape.comment(`ERROR ${next.source.id} ${next}`); } } @@ -39,17 +42,22 @@ export class TapeWriter implements IDest } } -export function test(name: string, cb: (t: tape.Test) => any) { +export function test(name: string, cb: (t: tape.Test, trace: TraceSource) => any) { tape(name, async t => { const writer = new TapeWriter(t); - TraceSource.on(ts => { + // this trace is not announced through the TraceSource global registry + const trace = new TraceSource(name); + trace.level = DebugLevel; + writer.writeEvents(trace.events); + + const h = TraceSource.on(ts => { ts.level = DebugLevel; writer.writeEvents(ts.events); }); try { - await cb(t); + await cb(t, trace); } catch (e) { // verbose error information @@ -60,6 +68,7 @@ export function test(name: string, cb: ( } finally { t.end(); destroy(writer); + destroy(h); } }); } diff --git a/src/test/ts/TextTests.ts b/src/test/ts/TextTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/TextTests.ts @@ -0,0 +1,86 @@ +import { StringBuilder } from "@implab/core/text/StringBuilder"; +import { test } from "./TestTraits"; +import { MockConsole } from "./mock/MockConsole"; +import { ConsoleWriter } from "@implab/core/log/ConsoleWriter"; + +test("String builder", async t => { + const sb = new StringBuilder(); + + sb.write("hello"); + t.equals(sb.toString(), "hello", "Write simple text"); + + sb.write(", "); + sb.write("world!"); + t.equals(sb.toString(), "hello, world!", "Append text"); + + sb.clear(); + t.equals(sb.toString(), "", "Clear"); + + sb.write(1); + t.equals(sb.toString(), "1", "Write number"); + + sb.clear(); + sb.writeValue(0.123); + t.equals(sb.toString(), "0.123", "Format number"); + + sb.clear(); + sb.writeValue(new Date("2019-01-02T00:00:00.000Z")); + t.equals(sb.toString(), "2019-01-02T00:00:00.000Z", "Format date (ISO)"); + + sb.clear(); + sb.write("{0}", "hello"); + t.equals(sb.toString(), "hello", "Simple format text"); + + sb.write(", {0}!", "world"); + t.equals(sb.toString(), "hello, world!", "Append formatted text"); + + sb.clear(); + sb.write("abc: {0:json}; {0.length}; {0.1} {{olo}}", ["a", "b", "c"]); + t.equals(sb.toString(), 'abc: [\n "a",\n "b",\n "c"\n]; 3; b {olo}', "Format string with spec"); + + sb.clear(); + t.throws(() => sb.write("}", 0), "Should die on bad format: '}'"); + t.throws(() => sb.write("{", 0), "Should die on bad format: '{'"); + t.throws(() => sb.write("{}", 0), "Should die on bad format: '{}'"); + t.throws(() => sb.write("{:}", 0), "Should die on bad format: '{:}'"); + t.throws(() => sb.write("{{0}", 0), "Should die on bad format: '{{0}'"); + +}); + +test("ConsoleWriter", t => { + const mockConsole = new MockConsole(); + const writer = new ConsoleWriter(mockConsole); + + writer.setLogLevel("log"); + + writer.writeLine("Hello, world!"); + + t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); + t.equals(mockConsole.getBuffer()[0].level, "log", "LogLevel should be 'log'"); + t.deepEqual(mockConsole.getBuffer()[0].data, ["Hello, world!"], "The buffer should contain single string"); + + mockConsole.clear(); + writer.setLogLevel("debug"); + writer.write("Bring "); + writer.write("the {0}!", "light"); + t.equals(mockConsole.getBuffer().length, 0, "No line should be written"); + writer.writeLine(); + + t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); + t.equals(mockConsole.getBuffer()[0].level, "debug", "LogLevel should be 'log'"); + t.deepEqual(mockConsole.getBuffer()[0].data, ["Bring the light!"], "Should concatenate string parts together"); + + mockConsole.clear(); + writer.writeLine("It's {0} o'clock, lets have some {1}!", { h: 5}, { title: "tee" }); + + t.deepEqual(mockConsole.getBuffer()[0].data, ["It's ", { h: 5}, " o'clock, lets have some ", { title: "tee" }, "!"], "Non string parts should be psassed as is"); + + mockConsole.clear(); + writer.writeLine("{0} or {1} to {2}", {i: 25}, 6, 4); + t.deepEqual(mockConsole.getBuffer()[0].data, [{i: 25}, " or 6 to 4"], "25 or 6 to 4"); + + mockConsole.clear(); + writer.writeLine("{0} or {1} to {2}! Let's have some {3}", 25, 6, 4, { product: "tee" } ); + t.deepEqual(mockConsole.getBuffer()[0].data, ["25 or 6 to 4! Let's have some ", { product: "tee" }], "Should handle many text chunks and object at the end"); + +}); diff --git a/src/test/ts/TraceSourceTests.ts b/src/test/ts/TraceSourceTests.ts --- a/src/test/ts/TraceSourceTests.ts +++ b/src/test/ts/TraceSourceTests.ts @@ -1,6 +1,9 @@ import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource"; import * as tape from "tape"; -import { TapeWriter } from "./TestTraits"; +import { TapeWriter, test } from "./TestTraits"; +import { MockConsole } from "./mock/MockConsole"; +import { ConsoleLog } from "@implab/core/log/writers/ConsoleLog"; +import { ConsoleWriter } from "@implab/core/log/ConsoleWriter"; const sourceId = "test/TraceSourceTests"; @@ -12,7 +15,7 @@ tape("trace message", t => { const h = trace.events.on(ev => { t.equal(ev.source, trace, "sender should be the current trace source"); t.equal(ev.level, DebugLevel, "level should be debug level"); - t.equal(ev.arg, "Hello, World!", "The message should be a formatted message"); + t.equal(ev.toString(), "Hello, World!", "The message should be a formatted message"); t.end(); }); @@ -34,7 +37,7 @@ tape("trace event", t => { const h = trace.events.on(ev => { t.equal(ev.source, trace, "sender should be the current trace source"); t.equal(ev.level, DebugLevel, "level should be debug level"); - t.equal(ev.arg, event, "The message should be the specified object"); + t.equal(ev.message, event, "The message should be the specified object"); t.end(); }); @@ -67,3 +70,17 @@ tape("tape comment writer", async t => { t.end(); }); + +test("console writer", (t, trace) => { + + const mockConsole = new MockConsole(); + const writer = new ConsoleWriter(mockConsole); + const consoleLog = new ConsoleLog(writer); + consoleLog.writeEvents(trace.events); + + trace.log("Hello, world!"); + t.deepEqual(mockConsole.getLine(0), ["console writer: Hello, world!"], "Log one string"); + + trace.log({ foo: "bar" }); + t.deepEqual(mockConsole.getLine(1), ["console writer: ", { foo: "bar" }], "Log an object"); +}); diff --git a/src/test/ts/mock/MockConsole.ts b/src/test/ts/mock/MockConsole.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/mock/MockConsole.ts @@ -0,0 +1,52 @@ +import {NullConsole} from "@implab/core/log/NullConsole"; + +interface ConsoleLineData { + level: string; + data: any[]; +} + +export class MockConsole extends NullConsole { + _buffer: ConsoleLineData[] = []; + + debug(...args: any[]) { + this._buffer.push({ + level: "debug", + data: args + }); + } + + log(...args: any[]) { + this._buffer.push({ + level: "log", + data: args + }); + } + + warn(...args: any[]) { + this._buffer.push({ + level: "warn", + data: args + }); + } + + error(...args: any[]) { + this._buffer.push({ + level: "error", + data: args + }); + } + + getBuffer() { + return this._buffer; + } + + getLine(i: number) { + if (i >= this._buffer.length) + throw new Error(`Line number ${i} is out of range, buffer.length = ${this._buffer.length}`); + return this._buffer[i].data; + } + + clear() { + this._buffer = []; + } +} diff --git a/src/testAmd/js/plan.js b/src/testAmd/js/plan.js --- a/src/testAmd/js/plan.js +++ b/src/testAmd/js/plan.js @@ -5,5 +5,6 @@ define([ "./CancellationTests", "./ObservableTests", "./ContainerTests", - "./SafeTests" + "./SafeTests", + "./TextTests" ]); \ No newline at end of file diff --git a/src/testCjs/ts/plan.ts b/src/testCjs/ts/plan.ts --- a/src/testCjs/ts/plan.ts +++ b/src/testCjs/ts/plan.ts @@ -3,3 +3,5 @@ import "./TraceSourceTests"; import "./CancellationTests"; import "./ObservableTests"; import "./ContainerTests"; +import "./SafeTests"; +import "./TextTests";