summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Welsh <contact@evanwelsh.com>2021-08-16 01:14:13 -0700
committerPhilip Chimento <philip.chimento@gmail.com>2021-08-16 20:29:02 -0700
commit924ff78052d2160878d202db98fe6eb9258f6a61 (patch)
tree5f7f1c275a6cfa1ca32dda2b268f7e10a85392b7
parent324319bfead0e63050b817c4e3543eba45ee9709 (diff)
downloadgjs-924ff78052d2160878d202db98fe6eb9258f6a61.tar.gz
modules: Implement WHATWG console specification
-rw-r--r--.eslintrc.yml1
-rw-r--r--installed-tests/js/.eslintrc.yml1
-rw-r--r--installed-tests/js/matchers.js36
-rw-r--r--installed-tests/js/meson.build1
-rw-r--r--installed-tests/js/testConsole.js256
-rw-r--r--js.gresource.xml1
-rw-r--r--modules/esm/_bootstrap/default.js2
-rw-r--r--modules/esm/console.js716
8 files changed, 1013 insertions, 1 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index dadf40bd..26bd8c74 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -259,5 +259,6 @@ globals:
window: readonly
TextEncoder: readonly
TextDecoder: readonly
+ console: readonly
parserOptions:
ecmaVersion: 2020
diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml
index cdf5cf9f..abc9c527 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -33,6 +33,7 @@ overrides:
- files:
- matchers.js
- testCairoModule.js
+ - testConsole.js
- testESModules.js
- testEncoding.js
- testGLibLogWriter.js
diff --git a/installed-tests/js/matchers.js b/installed-tests/js/matchers.js
index 6a2848f6..1e05828f 100644
--- a/installed-tests/js/matchers.js
+++ b/installed-tests/js/matchers.js
@@ -26,7 +26,41 @@ export function arrayLikeWithExactContents(elements) {
* @returns {string}
*/
jasmineToString() {
- return `${JSON.stringify(elements)}`;
+ return `<arrayLikeWithExactContents(${
+ elements.constructor.name
+ }[${JSON.stringify(Array.from(elements))}]>)`;
+ },
+ };
+}
+
+/**
+ * A jasmine asymmetric matcher which compares a given string to an
+ * array-like object of bytes. The compared bytes are decoded using
+ * TextDecoder and then compared using jasmine.stringMatching.
+ *
+ * @param {string | RegExp} text the text or regular expression to compare decoded bytes to
+ * @param {string} [encoding] the encoding of elements
+ * @returns
+ */
+export function decodedStringMatching(text, encoding = 'utf-8') {
+ const matcher = jasmine.stringMatching(text);
+
+ return {
+ /**
+ * @param {ArrayLike<number>} compareTo an array of bytes to decode and compare to
+ * @returns {boolean}
+ */
+ asymmetricMatch(compareTo) {
+ const decoder = new TextDecoder(encoding);
+ const decoded = decoder.decode(new Uint8Array(Array.from(compareTo)));
+
+ return matcher.asymmetricMatch(decoded, []);
+ },
+ /**
+ * @returns {string}
+ */
+ jasmineToString() {
+ return `<decodedStringMatching(${text})>`;
},
};
}
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index f85b9586..b42f3b20 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -241,6 +241,7 @@ endif
# minijasmine flag
modules_tests = [
+ 'Console',
'ESModules',
'Encoding',
'GLibLogWriter',
diff --git a/installed-tests/js/testConsole.js b/installed-tests/js/testConsole.js
new file mode 100644
index 00000000..95049d57
--- /dev/null
+++ b/installed-tests/js/testConsole.js
@@ -0,0 +1,256 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com>
+
+// eslint-disable-next-line
+/// <reference types="jasmine" />
+
+import GLib from 'gi://GLib';
+import {DEFAULT_LOG_DOMAIN} from 'console';
+
+import {decodedStringMatching} from './matchers.js';
+
+function objectContainingLogMessage(
+ message,
+ domain = DEFAULT_LOG_DOMAIN,
+ fields = {}
+) {
+ return jasmine.objectContaining({
+ MESSAGE: decodedStringMatching(message),
+ GLIB_DOMAIN: decodedStringMatching(domain),
+ ...fields,
+ });
+}
+
+describe('console', function () {
+ /** @type {jasmine.Spy<(_level: any, _fields: any) => any>} */
+ let writer_func;
+
+ /**
+ * @param {RegExp | string} message _
+ * @param {*} [logLevel] _
+ * @param {*} [domain] _
+ * @param {*} [fields] _
+ */
+ function expectLog(
+ message,
+ logLevel = GLib.LogLevelFlags.LEVEL_MESSAGE,
+ domain = DEFAULT_LOG_DOMAIN,
+ fields = {}
+ ) {
+ expect(writer_func).toHaveBeenCalledOnceWith(
+ logLevel,
+ objectContainingLogMessage(message, domain, fields)
+ );
+
+ // Always reset the calls, so that we can assert at the end that no
+ // unexpected messages were logged
+ writer_func.calls.reset();
+ }
+
+ beforeAll(function () {
+ writer_func = jasmine.createSpy(
+ 'Console test writer func',
+ function (level, _fields) {
+ if (level === GLib.LogLevelFlags.ERROR)
+ return GLib.LogWriterOutput.UNHANDLED;
+
+ return GLib.LogWriterOutput.HANDLED;
+ }
+ );
+
+ writer_func.and.callThrough();
+
+ // @ts-expect-error The existing binding doesn't accept any parameters because
+ // it is a raw pointer.
+ GLib.log_set_writer_func(writer_func);
+ });
+
+ beforeEach(function () {
+ writer_func.calls.reset();
+ });
+
+ it('has correct object tag', function () {
+ expect(console.toString()).toBe('[object console]');
+ });
+
+ it('logs a message', function () {
+ console.log('a log');
+
+ expect(writer_func).toHaveBeenCalledOnceWith(
+ GLib.LogLevelFlags.LEVEL_MESSAGE,
+ objectContainingLogMessage('a log')
+ );
+ writer_func.calls.reset();
+ });
+
+ it('logs a warning', function () {
+ console.warn('a warning');
+
+ expect(writer_func).toHaveBeenCalledOnceWith(
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ objectContainingLogMessage('a warning')
+ );
+ writer_func.calls.reset();
+ });
+
+ it('logs an informative message', function () {
+ console.info('an informative message');
+
+ expect(writer_func).toHaveBeenCalledOnceWith(
+ GLib.LogLevelFlags.LEVEL_INFO,
+ objectContainingLogMessage('an informative message')
+ );
+ writer_func.calls.reset();
+ });
+
+ describe('clear()', function () {
+ it('can be called', function () {
+ console.clear();
+ });
+
+ it('resets indentation', function () {
+ console.group('a group');
+ expectLog('a group');
+ console.log('a log');
+ expectLog(' a log');
+ console.clear();
+ console.log('a log');
+ expectLog('a log');
+ });
+ });
+
+ describe('table()', function () {
+ it('logs at least something', function () {
+ console.table(['title', 1, 2, 3]);
+ expectLog(/title/);
+ });
+ });
+
+ // %s - string
+ // %d or %i - integer
+ // %f - float
+ // %o - "optimal" object formatting
+ // %O - "generic" object formatting
+ // %c - "CSS" formatting (unimplemented by GJS)
+ describe('string replacement', function () {
+ const functions = {
+ log: GLib.LogLevelFlags.LEVEL_MESSAGE,
+ warn: GLib.LogLevelFlags.LEVEL_WARNING,
+ info: GLib.LogLevelFlags.LEVEL_INFO,
+ error: GLib.LogLevelFlags.LEVEL_CRITICAL,
+ };
+
+ Object.entries(functions).forEach(([fn, level]) => {
+ it(`console.${fn}() supports %s`, function () {
+ console[fn]('Does this %s substitute correctly?', 'modifier');
+ expectLog('Does this modifier substitute correctly?', level);
+ });
+
+ it(`console.${fn}() supports %d`, function () {
+ console[fn]('Does this %d substitute correctly?', 10);
+ expectLog('Does this 10 substitute correctly?', level);
+ });
+
+ it(`console.${fn}() supports %i`, function () {
+ console[fn]('Does this %i substitute correctly?', 26);
+ expectLog('Does this 26 substitute correctly?', level);
+ });
+
+ it(`console.${fn}() supports %f`, function () {
+ console[fn]('Does this %f substitute correctly?', 27.56331);
+ expectLog('Does this 27.56331 substitute correctly?', level);
+ });
+
+ it(`console.${fn}() supports %o`, function () {
+ console[fn]('Does this %o substitute correctly?', new Error());
+ expectLog(/Does this Error\n.*substitute correctly\?/s, level);
+ });
+
+ it(`console.${fn}() supports %O`, function () {
+ console[fn]('Does this %O substitute correctly?', new Error());
+ expectLog('Does this {} substitute correctly?', level);
+ });
+
+ it(`console.${fn}() ignores %c`, function () {
+ console[fn]('Does this %c substitute correctly?', 'modifier');
+ expectLog('Does this substitute correctly?', level);
+ });
+
+ it(`console.${fn}() supports mixing substitutions`, function () {
+ console[fn](
+ 'Does this %s and the %f substitute correctly alongside %d?',
+ 'string',
+ 3.14,
+ 14
+ );
+ expectLog(
+ 'Does this string and the 3.14 substitute correctly alongside 14?',
+ level
+ );
+ });
+
+ it(`console.${fn}() supports invalid numbers`, function () {
+ console[fn](
+ 'Does this support parsing %i incorrectly?',
+ 'a string'
+ );
+ expectLog('Does this support parsing NaN incorrectly?', level);
+ });
+
+ it(`console.${fn}() supports missing substitutions`, function () {
+ console[fn]('Does this support a missing %s substitution?');
+ expectLog(
+ 'Does this support a missing %s substitution?',
+ level
+ );
+ });
+ });
+ });
+
+ describe('time()', function () {
+ it('ends correctly', function (done) {
+ console.time('testing time');
+
+ // console.time logs nothing.
+ expect(writer_func).not.toHaveBeenCalled();
+
+ setTimeout(() => {
+ console.timeLog('testing time');
+
+ expectLog(/testing time: (.*)ms/);
+
+ console.timeEnd('testing time');
+
+ expectLog(/testing time: (.*)ms/);
+
+ console.timeLog('testing time');
+
+ expectLog(
+ "No time log found for label: 'testing time'.",
+ GLib.LogLevelFlags.LEVEL_WARNING
+ );
+
+ done();
+ }, 10);
+ });
+
+ it("doesn't log initially", function (done) {
+ console.time('testing time');
+
+ // console.time logs nothing.
+ expect(writer_func).not.toHaveBeenCalled();
+
+ setTimeout(() => {
+ console.timeEnd('testing time');
+ expectLog(/testing time: (.*)ms/);
+
+ done();
+ }, 10);
+ });
+
+ afterEach(function () {
+ // Ensure we only got the log lines that we expected
+ expect(writer_func).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/js.gresource.xml b/js.gresource.xml
index 947049c2..a730f2b8 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -16,6 +16,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 eb315af7..afb155b0 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -5,3 +5,5 @@
// Bootstrap the Encoding API
import '_encoding/encoding';
+// Bootstrap the Console API
+import 'console';
diff --git a/modules/esm/console.js b/modules/esm/console.js
new file mode 100644
index 00000000..152df43c
--- /dev/null
+++ b/modules/esm/console.js
@@ -0,0 +1,716 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com>
+
+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');
+
+const DEFAULT_LOG_DOMAIN = 'Gjs-Console';
+
+// A line-by-line implementation of https://console.spec.whatwg.org/.
+
+// 2.2.1. Formatting specifiers
+// https://console.spec.whatwg.org/#formatting-specifiers
+//
+// %s - string
+// %d or %i - integer
+// %f - float
+// %o - "optimal" object formatting
+// %O - "generic" object formatting
+// %c - "CSS" formatting (unimplemented by GJS)
+
+/**
+ * A simple regex to capture formatting specifiers
+ */
+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
+ * @returns {string}
+ */
+function formatOptimally(item) {
+ // Handle optimal error formatting.
+ if (item instanceof Error) {
+ return `${item.toString()}${item.stack ? '\n' : ''}${item.stack
+ ?.split('\n')
+ // Pad each stacktrace line.
+ .map(line => line.padStart(2, ' '))
+ .join('\n')}`;
+ }
+
+ // TODO: Enhance 'optimal' formatting.
+ // There is a current work on a better object formatter for GJS in
+ // https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/587
+ 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]
+ */
+
+/**
+ * Implementation of the WHATWG Console object.
+ *
+ * @implements {ConsoleInternalProps}
+ */
+// @ts-expect-error Console does not actually implement ConsoleInternalProps,
+// once private class fields are merged we will remove the interface.
+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),
+ },
+ });
+ }
+
+ get [Symbol.toStringTag]() {
+ return 'Console';
+ }
+
+ // 1.1 Logging functions
+ // https://console.spec.whatwg.org/#logging
+
+ /**
+ * Logs a critical message if the condition is not truthy.
+ * {@see console.error()} for additional information.
+ *
+ * @param {boolean} condition a boolean condition which, if false, causes
+ * the log to print
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ 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);
+ }
+
+ /**
+ * Resets grouping and clears the terminal on systems supporting ANSI
+ * terminal control sequences.
+ *
+ * In file-based stdout or systems which do not support clearing,
+ * console.clear() has no visual effect.
+ *
+ * @returns {void}
+ */
+ clear() {
+ this[sGroupIndentation] = '';
+ }
+
+ /**
+ * Logs a message with severity equal to {@see GLib.LogLevelFlags.DEBUG}.
+ *
+ * @param {...any} data formatting substitutions, if applicable
+ */
+ debug(...data) {
+ this[sLogger]('debug', data);
+ }
+
+ /**
+ * Logs a message with severity equal to {@see GLib.LogLevelFlags.CRITICAL}.
+ * Does not use {@see GLib.LogLevelFlags.ERROR} to avoid asserting and
+ * forcibly shutting down the application.
+ *
+ * @param {...any} data formatting substitutions, if applicable
+ */
+ error(...data) {
+ this[sLogger]('error', data);
+ }
+
+ /**
+ * Logs a message with severity equal to {@see GLib.LogLevelFlags.INFO}.
+ *
+ * @param {...any} data formatting substitutions, if applicable
+ */
+ info(...data) {
+ this[sLogger]('info', data);
+ }
+
+ /**
+ * Logs a message with severity equal to {@see GLib.LogLevelFlags.MESSAGE}.
+ *
+ * @param {...any} data formatting substitutions, if applicable
+ */
+ log(...data) {
+ this[sLogger]('log', data);
+ }
+
+ // 1.1.7 table(tabularData, properties)
+ table(tabularData, _properties) {
+ this.log(tabularData);
+ }
+
+ /**
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ 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],
+ });
+ }
+
+ /**
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ warn(...data) {
+ const {[sLogger]: Logger} = this;
+ Logger('warn', data);
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ dirxml(...data) {
+ this.log(...data);
+ }
+
+ // 1.2 Counting functions
+ // https://console.spec.whatwg.org/#counting
+
+ /**
+ * Logs how many times console.count(label) has been called with a given
+ * label.
+ * {@see console.countReset()} for resetting a count.
+ *
+ * @param {string} label unique identifier for this action
+ * @returns {void}
+ */
+ count(label) {
+ this[sCountLabels][label] = this[sCountLabels][label] ?? 0;
+ const count = ++this[sCountLabels][label];
+ const concat = `${label}: ${count}`;
+
+ this[sLogger]('count', [concat]);
+ }
+
+ /**
+ * @param {string} label the unique label to reset the count for
+ * @returns {void}
+ */
+ 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
+ // https://console.spec.whatwg.org/#grouping
+
+ /**
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ group(...data) {
+ const {[sLogger]: Logger} = this;
+
+ Logger('group', data);
+
+ this[sGroupIndentation] += ' ';
+ }
+
+ /**
+ * Alias for console.group()
+ *
+ * @param {...any} data formatting substitutions, if applicable
+ * @returns {void}
+ */
+ groupCollapsed(...data) {
+ // We can't 'collapse' output in a terminal, so we alias to
+ // group()
+ this.group(...data);
+ }
+
+ /**
+ * @returns {void}
+ */
+ groupEnd() {
+ this[sGroupIndentation] = this[sGroupIndentation].slice(0, -2);
+ }
+
+ // 1.4 Timing functions
+ // https://console.spec.whatwg.org/#timing
+
+ /**
+ * @param {string} label unique identifier for this action, pass to
+ * console.timeEnd() to complete
+ * @returns {void}
+ */
+ time(label) {
+ this[sTimeLabels][label] = GLib.get_monotonic_time();
+ }
+
+ /**
+ * Logs the time since the last call to console.time(label) where label is
+ * the same.
+ *
+ * @param {string} label unique identifier for this action, pass to
+ * console.timeEnd() to complete
+ * @param {...any} data string substitutions, if applicable
+ * @returns {void}
+ */
+ 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 durationMs = (GLib.get_monotonic_time() - startTime) / 1000;
+ const concat = `${label}: ${durationMs.toFixed(3)} ms`;
+ data.unshift(concat);
+
+ Printer('timeLog', data);
+ }
+ }
+
+ /**
+ * Logs the time since the last call to console.time(label) and completes
+ * the action.
+ * Call console.time(label) again to re-measure.
+ *
+ * @param {string} label unique identifier for this action
+ * @returns {void}
+ */
+ 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 {
+ delete this[sTimeLabels][label];
+
+ const durationMs = (GLib.get_monotonic_time() - startTime) / 1000;
+ const concat = `${label}: ${durationMs.toFixed(3)} ms`;
+
+ Printer('timeEnd', [concat]);
+ }
+ }
+
+ // Non-standard functions which are de-facto standards.
+ // Similar to Node, we define these as no-ops for now.
+
+ /**
+ * @deprecated Not implemented in GJS
+ *
+ * @param {string} _label unique identifier for this action, pass to
+ * console.profileEnd to complete
+ * @returns {void}
+ */
+ profile(_label) {}
+
+ /**
+ * @deprecated Not implemented in GJS
+ *
+ * @param {string} _label unique identifier for this action
+ * @returns {void}
+ */
+ profileEnd(_label) {}
+
+ /**
+ * @deprecated Not implemented in GJS
+ *
+ * @param {string} _label unique identifier for this action
+ * @returns {void}
+ */
+ timeStamp(_label) {}
+
+ // GJS-specific extensions for integrating with GLib structured logging
+
+ /**
+ * @param {string} logDomain the GLib log domain this Console should print
+ * with. Defaults to 'Gjs-Console'.
+ * @returns {void}
+ */
+ setLogDomain(logDomain) {
+ this[sLogDomain] = String(logDomain);
+ }
+
+ /**
+ * @returns {string}
+ */
+ get logDomain() {
+ return this[sLogDomain];
+ }
+
+ // 2. Supporting abstract operations
+ // https://console.spec.whatwg.org/#supporting-ops
+
+ /**
+ * 2.1. Logger
+ * https://console.spec.whatwg.org/#logger
+ *
+ * Conditionally applies formatting based on the inputted arguments,
+ * and prints at the provided severity (logLevel)
+ *
+ * @param {string} logLevel the severity (log level) 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.length === 0)
+ return;
+
+ let [first, ...rest] = args;
+
+ if (rest.length === 0) {
+ Printer(logLevel, [first]);
+ return undefined;
+ }
+
+ // If first does not contain any format specifiers, don't call Formatter
+ if (typeof first !== 'string' || !hasFormatSpecifiers(first)) {
+ Printer(logLevel, args);
+ return undefined;
+ }
+
+ // Otherwise, perform print the result of Formatter.
+ Printer(logLevel, Formatter([first, ...rest]));
+
+ return undefined;
+ }
+
+ /**
+ * 2.2. Formatter
+ * https://console.spec.whatwg.org/#formatter
+ *
+ * @param {[string, ...any[]]} args an array of format strings followed by
+ * their arguments
+ */
+ [sFormatter](args) {
+ const {[sFormatter]: Formatter} = this;
+
+ // The initial formatting string is the first arg
+ let target = args[0];
+
+ if (args.length === 1)
+ return target;
+
+ let current = args[1];
+
+ // Find the index of the first format specifier.
+ const specifierIndex = specifierTest.exec(target).index;
+ const specifier = target.slice(specifierIndex, specifierIndex + 2);
+ let converted = null;
+ switch (specifier) {
+ case '%s':
+ converted = String(current);
+ break;
+ case '%d':
+ case '%i':
+ if (typeof current === 'symbol')
+ converted = Number.NaN;
+ else
+ converted = parseInt(current, 10);
+ break;
+ case '%f':
+ if (typeof current === 'symbol')
+ converted = Number.NaN;
+ else
+ converted = parseFloat(current);
+ break;
+ case '%o':
+ converted = formatOptimally(current);
+ break;
+ case '%O':
+ converted = formatGenerically(current);
+ break;
+ case '%c':
+ converted = '';
+ break;
+ }
+ // If any of the previous steps set converted, replace the specifier in
+ // target with the converted value.
+ if (converted !== null) {
+ target =
+ target.slice(0, specifierIndex) +
+ converted +
+ target.slice(specifierIndex + 2);
+ }
+
+ /**
+ * Create the next format input...
+ *
+ * @type {[string, ...any[]]}
+ */
+ let result = [target, ...args.slice(2)];
+
+ if (!hasFormatSpecifiers(target))
+ return result;
+
+ if (result.length === 1)
+ return result;
+
+ return Formatter(result);
+ }
+
+ /**
+ * @typedef {object} PrinterOptions
+ * @param {string} [stackTrace] an error stacktrace to append
+ * @param {Record<string, any>} [fields] fields to include in the structured
+ * logging call
+ */
+
+ /**
+ * 2.3. Printer
+ * https://console.spec.whatwg.org/#printer
+ *
+ * This implementation of Printer maps WHATWG log severity to
+ * {@see GLib.LogLevelFlags} and outputs using GLib structured logging.
+ *
+ * @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 {PrinterOptions} [options] additional options for the
+ * printer
+ * @returns {void}
+ */
+ [sPrinter](logLevel, args, options) {
+ let severity;
+
+ switch (logLevel) {
+ case 'log':
+ case 'dir':
+ case 'dirxml':
+ case 'trace':
+ case 'group':
+ case 'groupCollapsed':
+ case 'timeLog':
+ case 'timeEnd':
+ severity = GLib.LogLevelFlags.LEVEL_MESSAGE;
+ break;
+ case 'debug':
+ severity = GLib.LogLevelFlags.LEVEL_DEBUG;
+ break;
+ case 'count':
+ case 'info':
+ severity = GLib.LogLevelFlags.LEVEL_INFO;
+ break;
+ case 'warn':
+ case 'countReset':
+ case 'reportWarning':
+ severity = GLib.LogLevelFlags.LEVEL_WARNING;
+ break;
+ case 'error':
+ case 'assert':
+ severity = GLib.LogLevelFlags.LEVEL_CRITICAL;
+ break;
+ default:
+ severity = GLib.LogLevelFlags.LEVEL_MESSAGE;
+ }
+
+ let output = args
+ .map(a => {
+ if (a === null)
+ return 'null';
+ else if (typeof a === 'object')
+ return formatOptimally(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,
+ },
+});
+
+const console = new Console();
+
+/**
+ * @param {string} domain set the GLib log domain for the global console object.
+ */
+function setConsoleLogDomain(domain) {
+ console.setLogDomain(domain);
+}
+
+/**
+ * @returns {string}
+ */
+function getConsoleLogDomain() {
+ return console.logDomain;
+}
+
+/**
+ * For historical web-compatibility reasons, the namespace object for
+ * console must have {} as its [[Prototype]].
+ *
+ * @type {Omit<Console, 'setLogDomain' | 'logDomain'>}
+ */
+const globalConsole = Object.create({});
+
+const propertyNames =
+ /** @type {['constructor', ...Array<string & keyof Console>]} */
+ // eslint-disable-next-line no-extra-parens
+ (Object.getOwnPropertyNames(Console.prototype));
+const propertyDescriptors = Object.getOwnPropertyDescriptors(Console.prototype);
+for (const key of propertyNames) {
+ if (key === 'constructor')
+ continue;
+
+ // This non-standard function shouldn't be included.
+ if (key === 'setLogDomain')
+ continue;
+
+ const descriptor = propertyDescriptors[key];
+ if (typeof descriptor.value !== 'function')
+ continue;
+
+ Object.defineProperty(globalConsole, key, {
+ ...descriptor,
+ value: descriptor.value.bind(console),
+ });
+}
+Object.defineProperties(globalConsole, {
+ [Symbol.toStringTag]: {
+ configurable: false,
+ enumerable: true,
+ get() {
+ return 'console';
+ },
+ },
+});
+Object.freeze(globalConsole);
+
+Object.defineProperty(globalThis, 'console', {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+ value: globalConsole,
+});
+
+export {
+ getConsoleLogDomain,
+ setConsoleLogDomain,
+ DEFAULT_LOG_DOMAIN
+};
+
+export default {
+ getConsoleLogDomain,
+ setConsoleLogDomain,
+ DEFAULT_LOG_DOMAIN,
+};