log/LogChannel.js

//Initialise functions
{
	if (!global.log)
		/**
		 * The namespace for all UF/Log utility functions, typically for designating log channels.
		 * 
		 * @namespace log
		 */
		global.log = {};
	
	/**
	 * Creates a new log channel that can be accessed by log.<channel_key>() for logging. Should ideally be initialised upon startup. This is used for custom multi-channel logging. Channels are mirrored in DevTools as well as having DOM-facing viewports and consoles via {@link ve.Log}, {@link ve.ScriptManager}.
	 * 
	 * ##### Constructor:
	 * - `arg0_key`; {@link string} - The key to use for channel logging. log.<channel_key>, log.<channel_key>_warn, log.<channel_key>_error are valid channels afterwards.
	 * - `arg1_options`: {@link Object}
	 *   - `.colour`: {@link Array}<{@link number}, {@link number}, {@link number}, {@link number}>|{@link string} - The colour to use for the background.
	 *   - `.do_not_print=false`: {@link boolean} - Whether to not print to console.
	 *   - `.large_object_limit=10000`: {@link number} - The limit at which large object warnings should be emitted.
	 *   - `.text_colour`: {@link Array}<{@link number}, {@link number}, {@link number}, {@link number}>|{@link string} - The text colour to use for the foreground. Detected as either 'white/'black' based on luminance by default.
	 * 
	 * ##### Instance:
	 * - `.log_el`: {@link HTMLElement}
	 * - `.key`: {@link string}
	 * - `.options`: {@link Object}
	 * 
	 * ##### Methods:
	 * - <span color=00ffff>{@link ve.Log.clear|clear}</span>()
	 * - <span color=00ffff>{@link ve.Log.close|close}</span>()
	 * - <span color=00ffff>{@link ve.Log.error|error}</span>(argn_arguments:{@link any})
	 * - <span color=00ffff>{@link ve.Log.fromJSON|fromJSON}</span>(arg0_json:{@link string})
	 * - <span color=00ffff>{@link ve.Log.log|log}</span>(argn_arguments:{@link any})
	 * - <span color=00ffff>{@link ve.Log.open|open}</span>()
	 * - <span color=00ffff>{@link ve.Log.warn|warn}</span>(argn_arguments:{@link any})
	 * - <span color=00ffff>{@link ve.Log.print|print}</span>(arg0_type:{@link string}, argn_arguments:{@link any})
	 * - <span color=00ffff>{@link ve.Log.remove|remove}</span>()
	 * - <span color=00ffff>{@link ve.Log.save|save}</span>(arg0_file_path:{@link string}, arg1_options:{@link Object}) | {@link string}
	 * - <span color=00ffff>{@link ve.Log.toJSON|toJSON}</span>() | {@link string}
	 * 
	 * ##### Static Fields:
	 * - `.instances`: {@link Array}<{@link log.Channel}>
	 *   
	 * ##### Static Methods:
	 * - <span color=00ffff>{@link ve.Log.update|update}</span>() - Updates all {@link ve.Log} instances and alphabetically sorts channel order.
	 * 
	 * @type {log.Channel}
	 */
	log.Channel = class {
		static instances = [];
		
		constructor (arg0_key, arg1_options) {
			//Convert from parameters
			let key = arg0_key;
			let options = (arg1_options) ? arg1_options : {};
			
			//Initialise options
			let bg_colour = (options.colour) ? 
				options.colour : ve.registry.settings.Channel.default_bg_colour;
			options.large_object_limit = Math.returnSafeNumber(options.large_object_limit, 10000);
			let text_colour = (options.text_colour) ? 
				options.text_colour : ve.registry.settings.Channel.default_text_colour;
				if (text_colour === "auto") text_colour = Colour.getBestTextColour(bg_colour);
			
			let default_post_css = `background: transparent; color: inherit;`;
			let default_pre_css = `background: ${bg_colour}; color: ${text_colour}; padding: 2px 5px; border-radius: 3px; font-weight: bold;`;
			
			if (options.do_not_print === undefined) options.do_not_print = false;
			options.post_css = (options.post_css) ? `${default_post_css} ${options.post_css}` : default_post_css;
			options.pre_css = (options.pre_css) ? `${default_pre_css} ${options.pre_css}` : default_pre_css;
			
			//Declare local instance variables
			this.log_el = document.createElement("div");
				this.log_el.classList.add("log-element");
			this.key = key;
			this.options = options;
			
			//Internal guard clause if duplicate
			if (!log[key]) {
				log[key] = this.log.bind(this);
				log[`${key}_error`] = this.error.bind(this);
				log[`${key}_instance`] = this;
				log[`${key}_warn`] = this.warn.bind(this);
			} else {
				console.warn(`log.${key} already exists as a custom logging channel. It cannot be overridden.`);
			}
			
			//Push to instances
			log.Channel.instances.push(this);
			log.Channel.update();
		}
		
		/**
		 * Clears the current console channel.
		 * - Method of: {@link log.Channel}
		 */
		clear () { this.log_el.innerHTML = ""; }
		
		/**
		 * Closes the available console window if already open.
		 * - Method of: {@link log.Channel}
		 */
		close () { if (this.console_window) this.console_window.close(); }
		
		/**
		 * Prints an error message to the console channel, analogous to {@link console.error}.
		 * - Method of: {@link log.Channel}
		 * 
		 * @param argn_arguments
		 */
		error (...argn_arguments) { this.print("error", argn_arguments); }
		
		/**
		 * Loads a log history from a JSON string and restores the internal HTML.
		 * - Method of: {@link log.Channel}
		 *
		 * @param {string} arg0_json - The JSON string to load.
		 */
		fromJSON (arg0_json) {
			//Convert from parameters
			let data = JSON.parse(arg0_json);
			
			//Hydrate current log from data
			if (data.key) this.key = data.key;
			if (data.options) this.options = data.options;
			if (data.html !== undefined) this.log_el.innerHTML = data.html;
		}
		
		/**
		 * Prints a log message to the console channel, analogous to {@link console.log}.
		 * - Method of: {@link log.Channel}
		 * 
		 * @param argn_arguments
		 */
		log (...argn_arguments) { this.print("log", argn_arguments); }
		
		/**
		 * Opens a UI for the given console channel. DOM-facing.
		 * - Method of: {@link log.Channel}
		 */
		open () {
			if (this.console_window) this.console_window.close();
			this.console_window = new ve.Window({
				current_log_el: new ve.HTML(this.log_el)
			}, {
				can_rename: false,
				name: this.key,
				width: "20rem"
			});
		}
		
		/**
		 * Prints a warning message to the console channel, analogous to {@link console.warn}.
		 * - Method of: {@link log.Channel}
		 *
		 * @param argn_arguments
		 */
		warn (...argn_arguments) { this.print("warn", argn_arguments); }
		
		/**
		 * Prints a message to the given console channel.
		 * - Method of: {@link log.Channel}
		 * 
		 * @param {string} arg0_type - The console type. Either 'log'/'warn'/'error'.
		 * @param {any[]} argn_arguments - The arguments passed to the log function
		 */
		print (arg0_type, argn_arguments) {
			//Convert from parameters
			let type = arg0_type;
			let args = [...argn_arguments];
			
			//Declare local instance variables
			let template = `%c${this.key.toUpperCase()}%c `;
			
			//If the first argument is a string, we can merge it into the template; this allows the user to still use %s, %d, etc. in their own messages
			if (!this.options.do_not_print)
				if (typeof args[0] === "string") {
					let message = args.shift();
					console[type](
						`${template}${message}`,
						this.options.pre_css,
						this.options.post_css,
						...args,
					);
				} else {
					console[type](template, this.options.pre_css, this.options.post_css, ...args);
				}
			
			//Push to current this.log_el
			let local_msg_el = document.createElement("div");
				local_msg_el.classList.add("uf-log-line");
				local_msg_el.classList.add(type);
				argn_arguments.forEach((local_arg) => {
					let part_el = document.createElement("span");
						part_el.classList.add("log-part");
						part_el.style.marginRight = `var(--padding)`;
						
					//Handle errors
					if (type === "error" && local_arg instanceof Error) {
						let error_el = document.createElement("pre");
						error_el.innerText = local_arg.stack || local_arg.message;
						part_el.appendChild(error_el);
					} else if (typeof local_arg === "string") {
						part_el.innerText = String(local_arg);
					} else {
						let local_object_inspector = new ve.ObjectInspector(local_arg, {
							style: { padding: 0 }
						});
						
						if (local_object_inspector.element.innerHTML.length > this.options.large_object_limit) {
							let placeholder = document.createElement("button");
							placeholder.innerText = loc("ve.registry.localisation.Log_show_large_object");
							placeholder.onclick = () => {
								let local_confirm_modal = new ve.Confirm(loc("ve.registry.localisation.Log_large_object_confirmation", String.formatNumber(local_object_inspector.element.innerHTML.length)), {
									special_function: () => {
										placeholder.replaceWith(local_object_inspector.element);
										local_object_inspector.bind(part_el);
									}
								});
							};
							part_el.appendChild(placeholder);
						} else {
							local_object_inspector.bind(part_el);
						}
					}
					
					local_msg_el.appendChild(part_el);
				});
			
			this.log_el.appendChild(local_msg_el);
		}
		
		/**
		 * Removes the given console channel.
		 * - Method of: {@link log.Channel}
		 */
		remove () {
			//Iterate over all channels and remove it
			for (let i = 0; i < log.Channel.instances.length; i++)
				if (log.Channel.instances[i] === this.key)
					log.Channel.instances.splice(i, 1);
			
			//Remove log[key]
			delete log[this.key];
			delete log[`${this.key}_error`];
			delete log[`${this.key}_instance`];
			delete log[`${this.key}_warn`];
			log.Channel.update();
		}
		
		/**
		 * Saves the present log to a given file path. Returns the output text written.
		 * - Method of: {@link log.Channel}
		 * 
		 * @param {string} arg0_file_path
		 * @param {Object} [arg1_options]
		 *   @param {string} [arg1_options.format="plaintext"] - Either 'json'/'plaintext'.
		 *   
		 * @returns {string}
		 */
		save (arg0_file_path, arg1_options) {
			//Convert from parameters
			let file_path = path.resolve(arg0_file_path);
			let options = (arg1_options) ? arg1_options : {};
			
			//Initialise options
			if (!options.format) options.format = "plaintext";
			
			//Declare local instance variables
			let output_folder = path.dirname(file_path); 
			let output_text;
			
			//Make sure folder exists first
			if (!fs.existsSync(output_folder))
				fs.mkdirSync(output_folder, { recursive: true });
			
			if (options.format === "json") {
				output_text = this.toJSON();
			} else {
				output_text = this.log_el.innerText;
			}
			fs.writeFileSync(file_path, output_text, "utf8");
			
			//Return statement
			return file_path;
		}
		
		/**
		 * Serialises the current log channel's entire history and configuration to a JSON string.
		 * - Method of: {@link log.Channel}
		 *
		 * @returns {string}
		 */
		toJSON () {
			//Return statement
			return JSON.stringify({
				key: this.key,
				options: this.options,
				html: this.log_el.innerHTML
			});
		}
		
		/**
		 * Updates all associated {@link log.Channel} components to ensure that they remain in-sync.
		 * 
		 * @alias #update
		 * @memberof log.Channel
		 */
		static update () {
			//Sort ve.Log.instances first
			ve.Log.instances.sort((a, b) => a.key.localeCompare(b.key));
			
			//Iterate over all ve.Log.instances and draw them
			for (let i = 0; i < ve.Log.instances.length; i++)
				ve.Log.instances[i].draw();
		}
	};
	
	/**
	 * Returns logging functions, resolving them either to a custom logging channel or to the default console.
	 * 
	 * @param {string} [arg0_channel_key="console"]
	 * 
	 * @returns {{error_fn: function, log_fn: function, warn_fn: function}}
	 */
	log.getLoggingFunctions = function (arg0_channel_key) {
		//Convert from parameters
		let channel_key = (arg0_channel_key) ? arg0_channel_key : "console";
		
		//Return statement
		return {
			error_fn: (channel_key !== "console") ? log[`${channel_key}_error`] : console.error,
			log_fn: (channel_key !== "console") ? log[`${channel_key}_log`] : console.log,
			warn_fn: (channel_key !== "console") ? log[`${channel_key}_warn`] : console.warn
		};
	};
}