[gjs/ewlsh/whatwg-console: 1/5] modules: Implement WHATWG console specification




commit 424fd8dd41afce60b8b03f388ac28bff4a05e357
Author: Evan Welsh <contact evanwelsh com>
Date:   Thu Jul 1 10:21:01 2021 -0700

    modules: Implement WHATWG console specification

 js.gresource.xml                  |   1 +
 modules/esm/_bootstrap/default.js |   3 +
 modules/esm/console.js            | 625 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 629 insertions(+)
---
diff --git a/js.gresource.xml b/js.gresource.xml
index 47be6425..95077155 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -12,6 +12,7 @@
   
     <file>modules/esm/cairo.js</file>
     <file>modules/esm/gettext.js</file>
+    <file>modules/esm/console.js</file>
     <file>modules/esm/gi.js</file>
     <file>modules/esm/system.js</file>
 
diff --git a/modules/esm/_bootstrap/default.js b/modules/esm/_bootstrap/default.js
index fefeb51b..8bf840d7 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -2,3 +2,6 @@
 // SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
 
 // Bootstrap file which supports ESM imports.
+
+// Import console API, console declares its global variables upon evaluation.
+import 'console';
diff --git a/modules/esm/console.js b/modules/esm/console.js
new file mode 100644
index 00000000..229684dd
--- /dev/null
+++ b/modules/esm/console.js
@@ -0,0 +1,625 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+// eslint-disable-next-line
+/// <reference lib='es2019' />
+
+// @ts-check
+
+// @ts-expect-error
+import GLib from 'gi://GLib';
+
+const sLogger = Symbol('Logger');
+const sPrinter = Symbol('Printer');
+const sFormatter = Symbol('Formatter');
+const sGroupIndentation = Symbol('GroupIndentation');
+const sTimeLabels = Symbol('Time Labels');
+const sCountLabels = Symbol('Count Labels');
+const sLogDomain = Symbol('Log Domain');
+
+export const DEFAULT_LOG_DOMAIN = 'Gjs-Console';
+
+// A line-by-line implementation of https://console.spec.whatwg.org/.
+
+// 2.2.1. Summary of formatting specifiers
+
+// The following is an informative summary of the format specifiers processed by
+// the above algorithm.
+// Specifier  Purpose                                                          Type Conversion
+// %s         Element which substitutes is converted to a string               %String%(element)
+// %d or %i   Element which substitutes is converted to an integer             %parseInt%(element, 10)
+// %f         Element which substitutes is converted to a float                %parseFloat%(element, 10)
+// %o         Element is displayed with optimally useful formatting            n/a
+// %O         Element is displayed with generic JavaScript object formatting   n/a
+// %c         Applies provided CSS                                             n/a
+const specifierTest = /%(d|i|s|f|o|O|c)/;
+
+/**
+ * @param {string} str a string to check for format specifiers like %s or %i
+ * @returns {boolean}
+ */
+function hasFormatSpecifiers(str) {
+    return specifierTest.test(str);
+}
+
+/**
+ * @param {any} item an item to format
+ */
+function formatGenerically(item) {
+    return JSON.stringify(item, null, 4);
+}
+
+/**
+ * @param {any} item an item to format
+ */
+function formatOptimally(item) {
+    // TODO: Consider 'optimal' formatting.
+    return JSON.stringify(item, null, 4);
+}
+
+const propertyAttributes = {
+    writable: true,
+    enumerable: false,
+    configurable: true,
+};
+
+/**
+ * @typedef ConsoleInternalProps
+ * @property {string} [sGroupIndentation]
+ * @property {Record<string, number>} [sCountLabels]
+ * @property {Record<string, number>} [sTimeLabels]
+ * @property {string} [sLogDomain]
+ */
+
+/**
+ * @implements {ConsoleInternalProps}
+ */
+// @ts-expect-error Console does not actually implement ConsoleInternalProps
+export class Console {
+    constructor() {
+        // Redefine the internal functions as non-enumerable.
+        Object.defineProperties(this, {
+            [sLogger]: {
+                ...propertyAttributes,
+                value: this[sLogger].bind(this),
+            },
+            [sFormatter]: {
+                ...propertyAttributes,
+                value: this[sFormatter].bind(this),
+            },
+            [sPrinter]: {
+                ...propertyAttributes,
+                value: this[sPrinter].bind(this),
+            },
+        });
+    }
+
+    // 1.1 Logging functions
+    // 1.1.1 assert(condition, ...data)
+    assert(condition, ...data) {
+        if (condition)
+            return;
+
+        let message = 'Assertion failed';
+
+        if (data.length === 0)
+            data.push(message);
+
+        if (typeof data[0] !== 'string') {
+            data.unshift(message);
+        } else {
+            const first = data.shift();
+            data.unshift(`${message}: ${first}`);
+        }
+        this[sLogger]('assert', data);
+    }
+
+    // 1.1.2 clear()
+    clear() {
+        throw new Error('clear() is not implemented.');
+    }
+
+    // 1.1.3 debug(...data)
+    debug(...data) {
+        this[sLogger]('debug', data);
+    }
+
+    // 1.1.4 error(...data)
+    error(...data) {
+        this[sLogger]('error', data);
+    }
+
+    // 1.1.5 info(...data)
+    info(...data) {
+        this[sLogger]('info', data);
+    }
+
+    // 1.1.6 log(...data)
+    log(...data) {
+        this[sLogger]('log', data);
+    }
+
+    // 1.1.7 table(tabularData, properties)
+    table(_tabularData, _properties) {
+        throw new Error('table() is not implemented.');
+    }
+
+    // 1.1.8 trace(...data)
+    trace(...data) {
+        if (data.length === 0)
+            data = ['Trace'];
+
+        this[sPrinter]('trace', data, {
+            stackTrace:
+                // We remove the first line to avoid logging this line as part of the trace.
+                new Error().stack?.split('\n', 2)?.[1],
+        });
+    }
+
+    // 1.1.9 warn(...data)
+    warn(...data) {
+        const {[sLogger]: Logger} = this;
+        Logger('warn', data);
+    }
+
+    // 1.1.10 dir(item, options)
+    /**
+     * @param {object} item an item to format generically
+     * @param {never} [options] any additional options for the formatter. Unused in our implementation.
+     */
+    dir(item, options) {
+        const object = formatGenerically(item);
+
+        this[sPrinter]('dir', [object], options);
+    }
+
+    // 1.1.11 dirxml(...data)
+    dirxml(...data) {
+        this.log(...data);
+    }
+
+    // 1.2 Counting functions
+
+    // 1.2.1 count(label)
+    count(label) {
+        this[sCountLabels][label] = this[sCountLabels][label] ?? 0;
+        const count = ++this[sCountLabels][label];
+        const concat = `${label}: ${String(count)}`;
+
+        this[sLogger]('count', [concat]);
+    }
+
+    // 1.2.2 countReset(label)
+    countReset(label) {
+        const {[sPrinter]: Printer} = this;
+
+        const count = this[sCountLabels][label];
+
+        if (typeof count !== 'number')
+            Printer('reportWarning', [`No count found for label: '${label}'.`]);
+        else
+            this[sCountLabels][label] = 0;
+    }
+
+    // 1.3 Grouping functions
+
+    // 1.3.1 group(...data)
+    group(...data) {
+        const {[sLogger]: Logger} = this;
+
+        Logger('group', data);
+
+        this[sGroupIndentation] += '  ';
+    }
+
+    // 1.3.2 groupCollapsed(...data)
+    groupCollapsed(...data) {
+        // We can't 'collapse' output in a terminal, so we alias to
+        // group()
+        this.group(...data);
+    }
+
+    // 1.3.3 groupEnd()
+    groupEnd() {
+        this[sGroupIndentation] = this[sGroupIndentation].slice(0, -2);
+    }
+
+    // 1.4 Timing functions
+
+    // 1.4.1 time(label)
+    time(label) {
+        this[sTimeLabels][label] = Date.now();
+    }
+
+    // 1.4.2 timeLog(label, ...data)
+    timeLog(label, ...data) {
+        const {[sPrinter]: Printer} = this;
+
+        const startTime = this[sTimeLabels][label];
+
+        if (typeof startTime !== 'number') {
+            Printer('reportWarning', [
+                `No time log found for label: '${label}'.`,
+            ]);
+        } else {
+            const duration = Date.now() - startTime;
+            const concat = `${label}: ${duration}ms`;
+            data.unshift(concat);
+
+            Printer('timeLog', data);
+        }
+    }
+
+    // 1.4.3 timeEnd(label)
+    timeEnd(label) {
+        const {[sPrinter]: Printer} = this;
+        const startTime = this[sTimeLabels][label];
+
+        if (typeof startTime !== 'number') {
+            Printer('reportWarning', [
+                `No time log found for label: '${label}'.`,
+            ]);
+        } else {
+            const duration = Date.now() - startTime;
+            const concat = `${label}: ${duration}ms`;
+
+            Printer('timeEnd', [concat]);
+        }
+    }
+
+    /**
+     * @param {string} logDomain the GLib log domain this Console should print
+     *   with. Defaults to Gjs-Console.
+     */
+    setLogDomain(logDomain) {
+        this[sLogDomain] = String(logDomain);
+    }
+
+    get logDomain() {
+        return this[sLogDomain];
+    }
+
+    // 2. Supporting abstract operations
+
+    /**
+     * 2.1. Logger(logLevel, args)
+     * The logger operation accepts a log level and a list of other arguments.
+     * Its main output is the implementation-defined side effect of printing the
+     * result to the console. This specification describes how it processes
+     * format specifiers while doing so.
+     *
+     * @param {string} logLevel the log level (log tag) the args should be
+     *   emitted with
+     * @param {unknown[]} args the arguments to pass to the printer
+     * @returns {void}
+     */
+    [sLogger](logLevel, args) {
+        const {[sFormatter]: Formatter, [sPrinter]: Printer} = this;
+
+        // If args is empty, return.
+        if (args.length === 0)
+            return;
+        // Let first be args[0].
+        // Let rest be all elements following first in args.
+        let [first, ...rest] = args;
+
+        // If rest is empty, perform Printer(logLevel, « first ») and return.
+        if (rest.length === 0) {
+            Printer(logLevel, [first]);
+            return undefined;
+        }
+        // If first does not contain any format specifiers, perform Printer(logLevel, args).
+        if (typeof first !== 'string' || !hasFormatSpecifiers(first)) {
+            Printer(logLevel, args);
+            return undefined;
+        }
+        // Otherwise, perform Printer(logLevel, Formatter(args)).
+        Printer(logLevel, Formatter([first, ...rest]));
+        // Return undefined.
+        return undefined;
+
+        // It’s important that the printing occurs before returning from the
+        // algorithm. Many developer consoles print the result of the last
+        // operation entered into them. In such consoles, when a developer
+        // enters console.log('hello!'), this will first print 'hello!', then
+        // the undefined return value from the console.log call.
+        // Indicating that printing is done before return
+    }
+
+    /**
+     * 2.2. Formatter(args)
+     *
+     * @param {[string, ...any[]]} args an array of format strings followed by
+     *   their arguments
+     */
+    [sFormatter](args) {
+        const {[sFormatter]: Formatter} = this;
+        // The formatter operation tries to format the first argument provided,
+        // using the other arguments. It will try to format the input until no
+        // formatting specifiers are left in the first argument, or no more
+        // arguments are left. It returns a list of objects suitable for
+        // printing.
+
+        // Let target be the first element of args.
+        let target = args[0];
+        // Let current be the second element of args.
+        let current = args[1];
+        // Find the first possible format specifier specifier, from the left to
+        // the right in target.
+        const specifierIndex = specifierTest.exec(target).index;
+        const specifier = target.slice(specifierIndex, specifierIndex + 2);
+        let converted = null;
+        switch (specifier) {
+        // If specifier is %s, let converted be the result of Call(%String%,
+        // undefined, « current »).
+        case '%s':
+            converted = String(current);
+            break;
+        // If specifier is %d or %i:
+        case '%d':
+        case '%i':
+            // If Type(current) is Symbol, let converted be NaN
+            if (typeof current === 'symbol')
+                converted = Number.NaN;
+            // Otherwise, let converted be the result of Call(%parseInt%,
+            // undefined, « current, 10 »).
+            else
+                converted = parseInt(current, 10);
+            break;
+        // If specifier is %f:
+        case '%f':
+            // If Type(current) is Symbol, let converted be NaN
+            if (typeof current === 'symbol')
+                converted = Number.NaN;
+            // Otherwise, let converted be the result of Call(%parseFloat%,
+            // undefined, « current »).
+            else
+                converted = parseFloat(current);
+            break;
+        // If specifier is %o, optionally let converted be current with
+        // optimally useful formatting applied.
+        case '%o':
+            converted = formatOptimally(current);
+            break;
+        // If specifier is %O, optionally let converted be current with generic
+        // JavaScript object formatting applied.
+        case '%O':
+            converted = formatGenerically(current);
+            break;
+        // TODO: process %c
+        case '%c':
+            break;
+        }
+        // If any of the previous steps set converted, replace specifier in
+        // target with converted.
+        if (converted !== null) {
+            target =
+                target.slice(0, specifierIndex) +
+                converted +
+                target.slice(specifierIndex + 2);
+        }
+        // Let result be a list containing target together with the elements of
+        // args starting from the third onward.
+        /** @type {[string, ...any[]]} */
+        let result = [target, ...args.slice(2)];
+        // If target does not have any format specifiers left, return result.
+        if (hasFormatSpecifiers(target))
+            return result;
+        // If result's size is 1, return result.
+        if (result.length === 1)
+            return result;
+        // Return Formatter(result).
+        return Formatter(result);
+    }
+
+    /**
+     * 2.3. Printer(logLevel, args[, options])
+     * The printer operation is implementation-defined. It accepts a log level
+     * indicating severity, a List of arguments to print, and an optional object
+     * of implementation-specific formatting options.
+     *
+     * Elements appearing in args will be one of the following:
+     * - JavaScript objects of any type.
+     * - Implementation-specific representations of printable things such as a
+     *   stack trace or group.
+     * - Objects with either generic JavaScript object formatting or optimally
+     *   useful formatting applied.
+     * - If the options object is passed, and is not undefined or null,
+     *   implementations may use options to apply implementation-specific
+     *   formatting to the elements in args.
+     *
+     * @param {string} logLevel the log level (log tag) the args should be
+     *   emitted with
+     * @param {unknown[]} args the arguments to print, either a format string
+     *   with replacement args or multiple strings
+     * @param {{ stackTrace?: string }} [options] additional options for the
+     *   printer
+     * @returns {void}
+     */
+    [sPrinter](logLevel, args, options) {
+        // How the implementation prints args is up to the implementation, but
+        // implementations should separate the objects by a space or something
+        // similar, as that has become a developer expectation.
+
+        // By the time the printer operation is called, all format specifiers
+        // will have been taken into account, and any arguments that are meant
+        // to be consumed by format specifiers will not be present in args. The
+        // implementation’s job is simply to print the List. The output produced
+        // by calls to Printer should appear only within the last group on the
+        // appropriate group stack if the group stack is not empty, or elsewhere
+        // in the console otherwise.
+
+        // If the console is not open when the printer operation is called,
+        // implementations should buffer messages to show them in the future up
+        // to an implementation-chosen limit (typically on the order of at least
+        // 100).
+
+        // 2.3.1. Indicating logLevel severity
+
+        // Each console function uses a unique value for the logLevel parameter
+        // when calling Printer, allowing implementations to customize each
+        // printed message depending on the function from which it originated.
+        // However, it is common practice to group together certain functions
+        // and treat their output similarly, in four broad categories. This
+        // table summarizes these common groupings:
+
+        // Grouping  console functions                 Description
+        // log       log(), trace(), dir(), dirxml(),  A generic log
+        //           dirxml(), group(),
+        //           groupCollapsed(), debug(),
+        //           timeLog()
+        // info      count(), info(), timeEnd()        An informative log
+        // warn      warn(), countReset()              A log warning the user of
+        //                                             something indicated by
+        //                                             the message
+        // error     error(), assert()                 A log indicating an error
+        //                                             to the user
+        let severity;
+
+        switch (logLevel) {
+        case 'log':
+        case 'dir':
+        case 'dirxml':
+        case 'trace':
+        case 'group':
+        case 'groupCollapsed':
+        case 'debug':
+        case 'timeLog':
+            severity = GLib.LogLevelFlags.LEVEL_MESSAGE;
+            break;
+        case 'count':
+        case 'info':
+        case 'timeEnd':
+            severity = GLib.LogLevelFlags.LEVEL_INFO;
+            break;
+        case 'warn':
+        case 'countReset':
+            severity = GLib.LogLevelFlags.LEVEL_WARNING;
+            break;
+        case 'error':
+        case 'assert':
+            severity = GLib.LogLevelFlags.LEVEL_CRITICAL;
+            break;
+        default:
+            severity = GLib.LogLevelFlags.LEVEL_MESSAGE;
+        }
+
+        // 2.4. Reporting warnings to the console
+
+        // To report a warning to the console given a generic description of a
+        // warning description, implementations must run these steps:
+
+        // Let warning be an implementation-defined string derived from
+        // description.
+
+        // Perform Printer('reportWarning', « warning »).
+        if (logLevel === 'reportWarning')
+            severity = GLib.LogLevelFlags.LEVEL_WARNING;
+
+        let output = args
+            .map(a => {
+                if (a === null)
+                    return 'null';
+                // TODO: Use a better object printer
+                else if (typeof a === 'object')
+                    return JSON.stringify(a);
+                else if (typeof a === 'function')
+                    return a.toString();
+                else if (typeof a === 'undefined')
+                    return 'undefined';
+                else if (typeof a === 'bigint')
+                    return `${a}n`;
+                else
+                    return String(a);
+            })
+            .join(' ');
+
+        let formattedOutput = this[sGroupIndentation] + output;
+
+        if (logLevel === 'trace') {
+            formattedOutput =
+                `${output}\n${options?.stackTrace}` ?? 'No trace available';
+        }
+
+        GLib.log_structured(this[sLogDomain], severity, {
+            MESSAGE: formattedOutput,
+        });
+    }
+}
+
+Object.defineProperties(Console.prototype, {
+    [sGroupIndentation]: {
+        ...propertyAttributes,
+        value: '',
+    },
+    [sCountLabels]: {
+        ...propertyAttributes,
+        /** @type {Record<string, number>} */
+        value: {},
+    },
+    [sTimeLabels]: {
+        ...propertyAttributes,
+        /** @type {Record<string, number>} */
+        value: {},
+    },
+    [sLogDomain]: {
+        ...propertyAttributes,
+        value: DEFAULT_LOG_DOMAIN,
+    },
+});
+
+export const console = new Console();
+
+/**
+ * @param {string} domain set the GLib log domain for the global console object.
+ */
+export function setConsoleLogDomain(domain) {
+    console.setLogDomain(domain);
+}
+
+/**
+ * @returns {string}
+ */
+export function getConsoleLogDomain() {
+    return console.logDomain;
+}
+
+// 1 Namespace console
+//
+// For historical web-compatibility reasons, the namespace object for console
+// must have as its [[Prototype]] an empty object, created as if by
+// ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%.
+const globalConsole = Object.create({});
+
+for (const [key, descriptor] of Object.entries(
+    Object.getOwnPropertyDescriptors(Console.prototype)
+)) {
+    if (key === 'constructor')
+        continue;
+    // This non-standard function shouldn't be included.
+    if (key === 'setLogDomain')
+        continue;
+
+    if (typeof descriptor.value !== 'function')
+        continue;
+
+    Object.defineProperty(globalConsole, key, {
+        ...descriptor,
+        value: descriptor.value.bind(console),
+    });
+}
+Object.freeze(globalConsole);
+
+Object.defineProperties(globalThis, {
+    console: {
+        configurable: false,
+        enumerable: true,
+        writable: false,
+        value: globalConsole,
+    },
+});
+
+export default {
+    console,
+    Console,
+};


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]