nadesiko3
Version:
Japanese Programming Language
256 lines (233 loc) • 9.42 kB
text/typescript
/** NakoLogger */
import { NakoError, NakoRuntimeError } from './nako_errors.mjs'
import { NakoColors } from './nako_colors.mjs'
import { Token, Ast } from './nako_types.mjs'
/**
* ログレベル - 数字が高いほど優先度が高い。
*/
export class LogLevel {
// level no
public static all = 0
public static trace = 1
public static debug = 2
public static info = 3
public static warn = 4
public static error = 5
public static stdout = 6
// string to level no
public static fromS(levelStr: string): number {
let level: number = LogLevel.trace
switch (levelStr) {
case 'all': level = LogLevel.all; break
case 'trace': level = LogLevel.trace; break
case 'debug': level = LogLevel.debug; break
case 'info': level = LogLevel.info; break
case 'warn': level = LogLevel.warn; break
case 'error': level = LogLevel.error; break
case 'stdout': level = LogLevel.stdout; break
default:
throw new Error('[NakoLogger] unknown logger level:' + levelStr)
}
return level
}
public static toString(level: number): string {
const levels: string[] = ['all', 'trace', 'debug', 'info', 'warn', 'error', 'stdout']
return levels[level]
}
}
/** Source Code Position */
interface Position {
startOffset: number;
endOffset: number;
line: number;
file: string;
}
type PositionOrNull = Position | Token | Ast | null;
interface LogListenerData {
position: PositionOrNull; // ログの送信時にposition引数で渡されたデータ
level: string; // ログの重要度(内部的にはnumber、入出力時stringに変換)
noColor: string; // 例: '[情報](2行目): foo'
nodeConsole: string; // 例: '\x1B[1m\x1B[34m[情報]\x1B[0m(2行目): foo\x1B[0m'
browserConsole: string[]; // 例: ['%c%c[情報]%c(2行目): foo', 'color: inherit; font-weight: bold;', ...]
html: string; // 例: '<div style="">...</div>'
}
type LogListener = (data: LogListenerData) => void;
/**
* エラー位置を日本語で表示する。
* たとえば `stringifyPosition({ file: "foo.txt", line: 5 })` は `"foo.txt(6行目):"` を出力する。
*/
function stringifyPosition(p: PositionOrNull): string {
if (!p) { return '' }
return `${p.file || ''}${p.line === undefined ? '' : `(${p.line + 1}行目): `}`
}
export function parsePosition(line: string): Position {
const m = line.match(/^l(\d+):(.+)/)
let lineNo = 0
let fileName = 'main.nako3'
if (m) {
lineNo = parseInt(m[1], 10)
fileName = m[2]
}
return {
startOffset: 0,
endOffset: 0,
line: lineNo,
file: fileName
}
}
interface NakoLoggerListener {
level: number;
callback: LogListener;
}
/**
* コンパイラのログ情報を出力するためのクラス。
* trace(), debug(), info(), warn(), error() はそれぞれメッセージに `[警告]` などのタグとエラー位置の日本語表現を付けて表示する。
* error() は引数にエラーオブジェクトを受け取ることもでき、その場合エラーオブジェクトからエラーメッセージとエラー位置が取り出される。
*/
export class NakoLogger {
private listeners: NakoLoggerListener[]
private logs: string
private position: string
public constructor() {
this.listeners = []
this.logs = ''
this.position = ''
}
public getErrorLogs(): [string, string] {
return [this.logs.replace(/\s+$/, ''), this.position]
}
public clear(): void {
this.logs = ''
this.position = ''
}
/**
* sendメソッドで送られた情報を受け取るコールバックを設定する。
* @param levelStr
* @param callback
*/
public addListener(levelStr: string, callback: LogListener): void {
const level: number = LogLevel.fromS(levelStr)
this.listeners.push({ level, callback })
}
/**
* addListenerメソッドで設定したコールバックを取り外す。
* @param {LogListener} callback
*/
public removeListener(callback: LogListener): void {
this.listeners = this.listeners.filter((l) => l.callback !== callback)
}
/** 本体開発時のデバッグ情報(debugより更に詳細な情報)
* @param {string} message
* @param {Position | null} position
*/
public trace(message: string, position: PositionOrNull = null):void {
this.sendI(LogLevel.trace, `${NakoColors.color.bold}[デバッグ情報(詳細)]${NakoColors.color.reset}${stringifyPosition(position)}${message}`, position)
}
/** 本体開発時のデバッグ情報
* @param {string} message
* @param {Position | null} position
*/
public debug(message: string, position: PositionOrNull = null): void {
this.sendI(LogLevel.debug, `${NakoColors.color.bold}[デバッグ情報]${NakoColors.color.reset}${stringifyPosition(position)}${message}`, position)
}
/** ユーザープログラムのデバッグ情報(あまり重要ではないもの)
* @param {string} message
* @param {Position | null} position
*/
public info(message: string, position: PositionOrNull = null): void {
this.sendI(LogLevel.info, `${NakoColors.color.bold}${NakoColors.color.blue}[情報]${NakoColors.color.reset}${stringifyPosition(position)}${message}`, position)
}
/** ユーザープログラムのデバッグ情報(重要なもの)
* @param {string} message
* @param {Position | null} position
*/
public warn(message: string, position: PositionOrNull = null):void {
this.sendI(LogLevel.warn, `${NakoColors.color.bold}${NakoColors.color.green}[警告]${NakoColors.color.reset}${stringifyPosition(position)}${message}`, position)
}
/** エラーメッセージ
* @param {string | Error} message
* @param {Position | null} position
*/
public error(message: string | Error | NakoError, position: PositionOrNull = null):void {
// NakoErrorか判定 (`message instanceof NakoError`では判定できない場合がある)
if (message instanceof Error && typeof (message as NakoError).type === 'string') {
// NakoErrorか
const etype: string = (message as NakoError).type
switch (etype) {
case 'NakoRuntimeError':
case 'NakoError':
if (message instanceof NakoError) {
const e: NakoError = message
let pos: any = position
if (pos === null || pos === undefined) {
pos = { file: e.file, line: e.line || 0, startOffset: 0, endOffset: 0 }
}
this.sendI(LogLevel.error, e.message, pos)
return
}
}
}
if (message instanceof Error) {
// 一般のエラーの場合は、messageのみ取得できる。
message = message.message
}
this.sendI(
LogLevel.error,
`${NakoColors.color.bold}${NakoColors.color.red}[エラー]${NakoColors.color.reset}${stringifyPosition(position)}${message}`,
position)
}
/** RuntimeErrorを生成する */
public runtimeError(error: any, posStr: string): NakoRuntimeError {
const e = new NakoRuntimeError(error, posStr)
return e
}
/** ユーザープログラムのデバッグ情報(すべて)
* @param {string} message
* @param {Position | null} position
*/
public stdout(message: string, position: PositionOrNull = null): void {
this.sendI(LogLevel.stdout, `${message}`, position)
}
/** 指定したlevelのlistenerにメッセージを送る。htmlやbrowserConsoleは無ければnodeConsoleから生成する。 */
public send(levelStr: string, nodeConsole: string, position: PositionOrNull, html: string|null = null, browserConsole: [string, string] | null = null): void {
const i = LogLevel.fromS(levelStr)
this.sendI(i, nodeConsole, position, html, browserConsole)
}
/** 指定したlevelのlistenerにメッセージを送る。htmlやbrowserConsoleは無ければnodeConsoleから生成する。 */
public sendI(level: number, nodeConsole: string, position: PositionOrNull, html: string|null = null, browserConsole: [string, string] | null = null): void {
const makeData = () => {
// nodeConsoleからnoColor, nodeCondoleなどの形式を生成する。
const formats = NakoColors.convertColorTextFormat(nodeConsole)
// ログが複数行から構成される場合は、htmlでの表現にborderを設定する。
let style = ''
if (nodeConsole.includes('\n')) {
style += 'border-top: 1px solid #8080806b; border-bottom: 1px solid #8080806b;'
}
// 各イベントリスナーが受け取るデータ
const data: LogListenerData = {
noColor: formats.noColor,
nodeConsole: formats.nodeConsole,
browserConsole: browserConsole || formats.browserConsole,
html: `<div style="${style}">` + (html || formats.html) + '</div>', // 各行を style: block で表示するために、<div>で囲む。
level: LogLevel.toString(level),
position
}
return data
}
// エラーならログに追加
if (level === LogLevel.error) {
const data = makeData()
this.logs += data.noColor + '\n'
if (position && this.position !== null) {
this.position = `l${position.line}:${position.file || 'unknown'}`
}
}
// 登録したリスナーに通知する
for (const l of this.listeners) {
if (l.level <= level) {
const data = makeData()
l.callback(data)
}
}
}
}