Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Last active February 16, 2024 01:51
Show Gist options
  • Save nyteshade/7b7f101c022ff98829be54d0c70200f6 to your computer and use it in GitHub Desktop.
Save nyteshade/7b7f101c022ff98829be54d0c70200f6 to your computer and use it in GitHub Desktop.
Read macos `mdls` content as JSON or nicely printed in terminal
convertFonts() {
if [[ ${#} -lt 1 ]]; then
echo "Usage: convertFonts <startDir> <outDir> [rename]"
echo "where"
echo " outDir - if missing, this is the same as startDir"
echo "\n\x1b[3mIf rename is \x1b[1mtrue\x1b[22m, old file name is mv'ed to the new name"
echo "otherwise a copy is made and the original is untouched.\x1b[23m"
else
startDir="${1:-.}"
outDir="${startDir}"
# Initialize rename to a default value (false or true, depending on your needs)
rename=false
if [[ $# -gt 2 ]]; then
# More than two arguments, check the last argument for 'true'
if [[ ${@: -1} = true ]]; then
rename=true
fi
# Assign the second to last argument to outDir
outDir="${@: -2:1}"
elif [[ $# -eq 2 ]]; then
# Exactly two arguments
if [[ ${@: -1} = true ]] || [[ ${@: -1} = false ]]; then
# If the last argument is explicitly true or false, assign it to rename
rename=${@: -1}
else
# Otherwise, assume it's the output directory
outDir="${@: -1}"
fi
fi
echo "First, true type fonts..."
for f in ${startDir}/**/*.ttf; do
mkdir -p "${outDir}/$(dirname ${f})"
out="${outDir}/$(dirname ${f})/$(mdls-json ${f} com_apple_ats_name_postscript reduce json)"
out="${out//\"/}.ttf"
rmMsg=" \x1b[1m[\x1b[22;33mcopied\x1b[39;1m]\x1b[22m"
if [[ -e "${out}" ]]; then
rmMsg=" \x1b[1m[\x1b[22;34mskipped - already exists\x1b[39;1m]\x1b[22m"
else
if [[ ${2:-false} = true ]]; then
mv "${f}" "${out}"
rmMsg=" \x1b[1m[\x1b[22;32mrenamed\x1b[39;1m]\x1b[22m"
else
cp -fp "${f}" "${out}"
fi
fi
echo " ${f} -> ${out//\"/}${rmMsg}"
done
echo "\nNext, open type fonts..."
for f in ${1:-.}/**/*.otf; do
mkdir -p "${outDir}/$(dirname ${f})"
out="${outDir}/$(dirname ${f})/$(mdls-json ${f} com_apple_ats_name_postscript reduce json)"
out="${out//\"/}.otf"
rmMsg=" \x1b[1m[\x1b[22;33mcopied\x1b[39;1m]\x1b[22m"
if [[ -e "${out}" ]]; then
rmMsg=" \x1b[1m[\x1b[22;34mskipped - already exists\x1b[39;1m]\x1b[22m"
else
if [[ ${2:-false} = true ]]; then
mv "${f}" "${out}"
rmMsg=" \x1b[1m[\x1b[22;32mrenamed\x1b[39;1m]\x1b[22m"
else
cp -fp "${f}" "${out}"
fi
fi
echo " ${f} -> ${out//\"/}${rmMsg}"
done
fi
}
#!/usr/bin/env node
const { execSync } = require('child_process');
const { existsSync } = require('fs');
const { resolve, join } = require('path');
const { inspect } = require('util');
/**
* Represents a parser for macOS's `mdls` command output, facilitating the
* extraction of metadata from files. This class can either directly parse
* pre-fetched metadata provided to it, or fetch and parse metadata from a
* specified file path using the `mdls` command. It is designed to simplify
* interactions with file metadata on macOS by abstracting away the command line
* operations and parsing logic into an easy-to-use interface.
*/
class MDLSParser {
/**
* Constructs an instance of MDLSParser, which parses metadata of a file using
* macOS's `mdls` command. The constructor either takes a file path and
* attempts to resolve and read the file's metadata, or directly takes
* pre-fetched metadata.
*
* @param {Object} options - Configuration options for the parser.
* @param {string} [options.filePath] - The path to the file whose metadata is
* to be parsed. If provided, the constructor attempts to resolve the path and
* fetch metadata using `mdls`.
* @param {string} [options.mdlsOutput] - Pre-fetched metadata as a string. If
* provided, `filePath` is ignored, and this metadata is used directly.
*
* @throws {Error} Throws an error if neither `filePath` nor `mdlsOutput` is
* provided, or if the file at `filePath` cannot be found.
*/
constructor({ filePath, mdlsOutput }) {
if (filePath && !mdlsOutput) {
let pathToUse = undefined;
const tryThese = [
filePath,
resolve(filePath),
resolve(join('.', filePath))
];
for (let path of tryThese) {
if (existsSync(path)) {
pathToUse = path;
break;
}
}
if (!pathToUse) {
console.error('Unable to find the supplied file at any of these places');
tryThese.forEach(console.error);
throw new Error('Cannot find path to file to hand to mdls.');
}
this.filePath = pathToUse;
this.mdlsOutput = execSync(`mdls '${pathToUse.replace(/'/g,"\'")}'`)
.toString();
}
else if (mdlsOutput) {
this.filePath = undefined;
this.mdlsOutput = String(mdlsOutput);
}
else {
console.error([
'MDLSParser requires an object with either `filePath` ',
'or `mdlsOutut` for input'
].join(''));
throw new Error('Invalid input to call to new MDLSParser()');
}
this.output = this.parse();
}
/**
* Parses the `mdlsOutput` string to extract metadata into a JSON object.
* This method iterates over the `mdlsOutput` string, identifying and
* extracting key-value pairs of metadata. The keys are identified by patterns
* ending with " = ", and values are parsed based on their format: null,
* empty string, array, or other (including numbers and strings). Arrays are
* specially handled by extracting the substring enclosed in parentheses and
* splitting it into items. This method updates the `output` property of the
* instance with the parsed JSON object.
*
* @returns {Object} The parsed metadata as a JSON object. This object is also
* assigned to the `output` property of the instance.
*/
parse() {
const { extractSubstring } = this.constructor;
const { mdlsOutput } = this;
let offset = 0;
let jsonData = {};
while (offset < mdlsOutput.length) {
let keyMatch = mdlsOutput.substring(offset).match(/^(.*?) = /m);
if (!keyMatch) break;
let key = keyMatch[1];
offset += keyMatch.index + keyMatch[0].length;
key = key.trim();
if (mdlsOutput.substring(offset).slice(0,6) === '(null)') {
jsonData[key] = null;
offset += 6;
}
else if (mdlsOutput.substring(offset).slice(0,3) === '""\n') {
jsonData[key] = '';
}
else if (mdlsOutput[offset] === '(') {
let { extracted, newOffset } = extractSubstring(mdlsOutput, offset, ['(', ')']);
let arrayValue = extracted
.slice(1, -1)
.split(/\s*,[\n]?\s*/)
.map(item => item.trim().replace(/^"|"$/g, ''));
jsonData[key] = arrayValue;
offset = newOffset;
}
else {
let valueMatch = mdlsOutput.substring(offset).match(/^(.*?)(\n|$)/);
if (!valueMatch) break;
let value = valueMatch[1].trim().replace(/^"|"$/g, '');
jsonData[key] = isNaN(Number(value)) ? value : Number(value);
offset += valueMatch.index + valueMatch[0].length;
}
}
return (this.output = jsonData);
}
/**
* Provides a dynamic view of the `output` property, allowing for controlled
* access and interaction with its properties. This getter returns a Proxy
* that intercepts and defines custom behavior for fundamental operations
* (e.g., property lookup, enumeration, and property check). The Proxy is
* particularly useful for adding custom logic to access the properties of
* the `output` object.
*
* The Proxy defines the following traps:
* - `get`: Returns the value of the property if it exists. If the property
* being accessed is `Symbol.toStringTag`, it returns 'MDLSParserOutput'.
* - `has`: Checks if a property exists in the `output` object.
* - `ownKeys`: Returns an array of the property names owned by the `output`
* object.
*
* @returns {Proxy} A Proxy wrapping the `output` object, with custom behavior
* defined for property access, existence checks, and property enumeration.
*/
get get() {
const className = this.constructor.name;
const targetObj = this.output && typeof this.output === 'object'
? this.output
: {};
const inspectFn = (target) => (depth = 2, options = { color: true }) => {
const data = inspect({
'keyCount': Reflect.ownKeys(target).length
}, { ...options, depth: depth - 1 }).replaceAll(/'/g, '');
return `${className}Output ${data}`;
};
const proxy = new Proxy(targetObj, {
get(target, property, receiver) {
if (property === Symbol.toStringTag) {
return `${className}Output`
}
if (property === Symbol.for('receiver')) {
return receiver;
}
if (property === MDLSParser.nodeInspect) {
return inspectFn(target);
}
return target[property];
},
has(target, property) {
if (
target === Symbol.toStringTag ||
target === MDLSParser.nodeInspect
) {
return true;
}
return Reflect.ownKeys(target, property);
},
ownKeys(target) {
return Reflect.ownKeys(target).concat([
Symbol.toStringTag,
MDLSParser.nodeInspect
]);
},
});
const descriptor = {
value: inspect(targetObj),
enumerable: false,
configurable: true,
};
for (const obj of [proxy, proxy[Symbol.for('receiver')]]) {
Object.defineProperty(obj, MDLSParser.nodeInspect, descriptor);
}
return proxy;
}
/**
* Retrieves the keys of the `output` object as an array. If the `output`
* object is not defined, it returns an empty array. This getter is useful
* for quickly accessing the keys of the parsed data stored in the `output`
* property without needing to directly interact with the `output` object
* itself. It simplifies the process of enumerating the keys present in the
* `output`, providing a straightforward way to iterate over or inspect the
* data structure's properties.
*
* @returns {Array<string>} An array of strings representing the keys of the
* `output` object. Returns an empty array if `output` is undefined.
*/
get keys() {
return Object.keys(this?.output ?? {});
}
/**
* Retrieves the values of the `output` object as an array. If the `output`
* object is not defined, it returns an empty array. This getter simplifies
* accessing the values of the parsed data stored in the `output` property,
* allowing for easy iteration over or inspection of the data structure's
* values without direct interaction with the `output` object itself.
*
* @returns {Array<any>} An array containing the values of the `output`
* object. Returns an empty array if `output` is undefined.
*/
get values() {
return Object.values(this?.output ?? {});
}
/**
* Retrieves the entries of the `output` object as an array of [key, value]
* pairs. If the `output` object is not defined, it returns an empty array.
* This getter simplifies accessing both the keys and values of the parsed
* data stored in the `output` property, allowing for easy iteration over
* or inspection of the data structure's entries without direct interaction
* with the `output` object itself.
*
* @returns {Array<[string, any]>} An array containing the entries of the
* `output` object as [key, value] pairs. Returns an empty array if `output`
* is undefined.
*/
get entries() {
return Object.entries(this?.output ?? {});
}
/**
* Custom iterator generator for the MDLSParser instance. This iterator
* allows for easy iteration over the `output` object's entries (key-value
* pairs) using the `for...of` loop or other iterable protocols. If the
* `output` object is not defined or is not an object, the iterator will
* not yield any values, effectively behaving as an iterator over an empty
* collection.
*
* @yields {[string, any]} Yields an array containing a key-value pair
* [key, value] from the `output` object for each iteration.
*/
*[Symbol.iterator]() {
if (!this.output || typeof this.output !== 'object') {
return;
}
for (const entry of Object.entries(this.output)) {
yield entry;
}
}
/**
* A getter for the default string description of the MDLSParser instance.
* This property is used in object-to-string conversions and is accessed
* internally by methods like `Object.prototype.toString`. It simplifies
* identifying the type of the MDLSParser instance in debugging or logging
* scenarios. By default, it returns the name of the constructor, aiding
* in quickly identifying the object's type in console outputs or logs.
*
* @returns {string} The name of the constructor of the instance, which
* helps in identifying the object's type.
*/
get [Symbol.toStringTag]() { return this.constructor.name; }
/**
* Custom inspection function for Node.js `util.inspect` that formats the
* MDLSParser instance's information. This method is invoked when
* `util.inspect` is called on an instance of MDLSParser, providing a
* customized string representation of the instance. It includes the length
* of the `mdlsOutput` and the number of keys in the `output` object.
*
* @param {number} depth The depth to which `util.inspect` will recurse.
* @param {object} options Formatting options passed to `util.inspect`.
* @param {function} inspect Reference to the `util.inspect` function.
* @returns {string} A formatted string representing the MDLSParser instance.
*/
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
const data = inspect({
'mdlsOutput (length)': this.mdlsOutput.length,
'parsed (keys)': Reflect.ownKeys(this.output).length,
}, { ...options, depth: depth - 1 }).replaceAll(/'/g, '');
return `MDLSParser ${data}`;
}
/**
* Extracts a substring from `input` starting at `offset` and enclosed by
* `tokens`. This method is designed to handle nested structures and can
* ignore escaped tokens within strings. It's particularly useful for parsing
* complex data formats or extracting nested information.
*
* @param {string} input The string from which to extract the substring.
* @param {number} offset The position in `input` to start searching for
* the substring.
* @param {[string, string]} tokens An array containing two elements: the
* opening and closing tokens that define the boundaries of the substring.
* @returns {{extracted: string|null, newOffset: number}} An object
* containing the `extracted` substring (null if not found) and `newOffset`,
* the position after the end of the extracted substring. If no valid
* substring is found, `newOffset` will be the same as the input `offset`.
*/
static extractSubstring(input, offset, tokens) {
let [openToken, closeToken] = tokens;
let depth = 0;
let start = -1;
let end = -1;
let escapeNext = false;
let inString = openToken === '"' && closeToken === '"';
for (let i = offset; i < input.length; i++) {
const char = input[i];
if (inString && char === '\\' && !escapeNext) {
escapeNext = true;
continue;
}
if (char === openToken && !escapeNext) {
depth++;
if (start === -1) start = i;
} else if (char === closeToken && !escapeNext) {
depth--;
if (depth === 0) {
end = i;
break;
}
}
escapeNext = false;
}
if (start !== -1 && end !== -1) {
const extracted = inString
? input.slice(start + 1, end)
: input.slice(start, end + 1);
return { extracted, newOffset: end + 1 };
} else {
return { extracted: null, newOffset: offset };
}
}
/**
* Provides a custom inspection symbol for Node.js's `util.inspect` method.
* This enables defining custom object representations in the output of
* `util.inspect`, useful for debugging or logging. When an object includes
* a method keyed by this symbol, it customizes how the object is represented.
*
* @returns {symbol} Symbol for custom inspection in Node.js.
*/
static get nodeInspect() {
return Symbol.for('nodejs.util.inspect.custom');
}
}
const interpreter = process.argv[0];
const script = process.argv[1];
const args = process.argv.slice(2);
const filePath = process.argv[2];
if (!args.length) {
console.error(`Usage: ${process.argv[1]} <file_path> [<top-level-key-name>] [<index>] [json]\n`);
console.error(`\x1b[3mIf "json" is not specified, the output will be raw colorized output`);
console.error(`which will be harder to parse and work with. Specify "json" if you are scripting`);
console.error(`the output.\x1b[23m`);
process.exit(1);
}
args.hasRemove = function(predicate, valueIfMissing) {
let value = this.find(predicate);
let index = this.indexOf(value);
if (~index) {
this.splice(index,1);
}
return ~index ? value : valueIfMissing;
}
const mdls = new MDLSParser({ filePath });
const useJSON = args.hasRemove(k => k.toLowerCase() === 'json', false);
const useColor = !useJSON;
const format = (output) => useColor ? inspect(output, { colors: true }) : JSON.stringify(output, null, 2);
if (args.length === 1) {
console.log(format(mdls.output));
}
else {
const index = args.hasRemove(v => !isNaN(Number(v)));
const key = args[1];
const value = Array.isArray(mdls.output[key]) && index !== undefined
? mdls.output[key][index]
: mdls.output[key];
console.log(format(value));
}
const { execSync } = require('child_process');
const { existsSync } = require('fs');
const { resolve, join } = require('path');
const { inspect } = require('util');
/**
* Represents a parser for macOS's `mdls` command output, facilitating the
* extraction of metadata from files. This class can either directly parse
* pre-fetched metadata provided to it, or fetch and parse metadata from a
* specified file path using the `mdls` command. It is designed to simplify
* interactions with file metadata on macOS by abstracting away the command line
* operations and parsing logic into an easy-to-use interface.
*/
class MDLSParser {
/**
* Constructs an instance of MDLSParser, which parses metadata of a file using
* macOS's `mdls` command. The constructor either takes a file path and
* attempts to resolve and read the file's metadata, or directly takes
* pre-fetched metadata.
*
* @param {Object} options - Configuration options for the parser.
* @param {string} [options.filePath] - The path to the file whose metadata is
* to be parsed. If provided, the constructor attempts to resolve the path and
* fetch metadata using `mdls`.
* @param {string} [options.mdlsOutput] - Pre-fetched metadata as a string. If
* provided, `filePath` is ignored, and this metadata is used directly.
*
* @throws {Error} Throws an error if neither `filePath` nor `mdlsOutput` is
* provided, or if the file at `filePath` cannot be found.
*/
constructor({ filePath, mdlsOutput }) {
if (filePath && !mdlsOutput) {
let pathToUse = undefined;
const tryThese = [
filePath,
resolve(filePath),
resolve(join('.', filePath))
];
for (let path of tryThese) {
if (existsSync(path)) {
pathToUse = path;
break;
}
}
if (!pathToUse) {
console.error('Unable to find the supplied file at any of these places');
tryThese.forEach(console.error);
throw new Error('Cannot find path to file to hand to mdls.');
}
this.filePath = pathToUse;
this.mdlsOutput = execSync(`mdls '${pathToUse.replace(/'/g,"\'")}'`)
.toString();
}
else if (mdlsOutput) {
this.filePath = undefined;
this.mdlsOutput = String(mdlsOutput);
}
else {
console.error([
'MDLSParser requires an object with either `filePath` ',
'or `mdlsOutut` for input'
].join(''));
throw new Error('Invalid input to call to new MDLSParser()');
}
this.output = this.parse();
}
/**
* Parses the `mdlsOutput` string to extract metadata into a JSON object.
* This method iterates over the `mdlsOutput` string, identifying and
* extracting key-value pairs of metadata. The keys are identified by patterns
* ending with " = ", and values are parsed based on their format: null,
* empty string, array, or other (including numbers and strings). Arrays are
* specially handled by extracting the substring enclosed in parentheses and
* splitting it into items. This method updates the `output` property of the
* instance with the parsed JSON object.
*
* @returns {Object} The parsed metadata as a JSON object. This object is also
* assigned to the `output` property of the instance.
*/
parse() {
const { extractSubstring } = this.constructor;
const { mdlsOutput } = this;
let offset = 0;
let jsonData = {};
while (offset < mdlsOutput.length) {
let keyMatch = mdlsOutput.substring(offset).match(/^(.*?) = /m);
if (!keyMatch) break;
let key = keyMatch[1];
offset += keyMatch.index + keyMatch[0].length;
key = key.trim();
if (mdlsOutput.substring(offset).slice(0,6) === '(null)') {
jsonData[key] = null;
offset += 6;
}
else if (mdlsOutput.substring(offset).slice(0,3) === '""\n') {
jsonData[key] = '';
}
else if (mdlsOutput[offset] === '(') {
let { extracted, newOffset } = extractSubstring(mdlsOutput, offset, ['(', ')']);
let arrayValue = extracted
.slice(1, -1)
.split(/\s*,[\n]?\s*/)
.map(item => item.trim().replace(/^"|"$/g, ''));
jsonData[key] = arrayValue;
offset = newOffset;
}
else {
let valueMatch = mdlsOutput.substring(offset).match(/^(.*?)(\n|$)/);
if (!valueMatch) break;
let value = valueMatch[1].trim().replace(/^"|"$/g, '');
jsonData[key] = isNaN(Number(value)) ? value : Number(value);
offset += valueMatch.index + valueMatch[0].length;
}
}
return (this.output = jsonData);
}
/**
* Provides a dynamic view of the `output` property, allowing for controlled
* access and interaction with its properties. This getter returns a Proxy
* that intercepts and defines custom behavior for fundamental operations
* (e.g., property lookup, enumeration, and property check). The Proxy is
* particularly useful for adding custom logic to access the properties of
* the `output` object.
*
* The Proxy defines the following traps:
* - `get`: Returns the value of the property if it exists. If the property
* being accessed is `Symbol.toStringTag`, it returns 'MDLSParserOutput'.
* - `has`: Checks if a property exists in the `output` object.
* - `ownKeys`: Returns an array of the property names owned by the `output`
* object.
*
* @returns {Proxy} A Proxy wrapping the `output` object, with custom behavior
* defined for property access, existence checks, and property enumeration.
*/
get get() {
const className = this.constructor.name;
const targetObj = this.output && typeof this.output === 'object'
? this.output
: {};
const inspectFn = (target) => (depth = 2, options = { color: true }) => {
const data = inspect({
'keyCount': Reflect.ownKeys(target).length
}, { ...options, depth: depth - 1 }).replaceAll(/'/g, '');
return `${className}Output ${data}`;
};
const proxy = new Proxy(targetObj, {
get(target, property, receiver) {
if (property === Symbol.toStringTag) {
return `${className}Output`
}
if (property === Symbol.for('receiver')) {
return receiver;
}
if (property === MDLSParser.nodeInspect) {
return inspectFn(target);
}
return target[property];
},
has(target, property) {
if (
target === Symbol.toStringTag ||
target === MDLSParser.nodeInspect
) {
return true;
}
return Reflect.ownKeys(target, property);
},
ownKeys(target) {
return Reflect.ownKeys(target).concat([
Symbol.toStringTag,
MDLSParser.nodeInspect
]);
},
});
const descriptor = {
value: inspect(targetObj),
enumerable: false,
configurable: true,
};
for (const obj of [proxy, proxy[Symbol.for('receiver')]]) {
Object.defineProperty(obj, MDLSParser.nodeInspect, descriptor);
}
return proxy;
}
/**
* Retrieves the keys of the `output` object as an array. If the `output`
* object is not defined, it returns an empty array. This getter is useful
* for quickly accessing the keys of the parsed data stored in the `output`
* property without needing to directly interact with the `output` object
* itself. It simplifies the process of enumerating the keys present in the
* `output`, providing a straightforward way to iterate over or inspect the
* data structure's properties.
*
* @returns {Array<string>} An array of strings representing the keys of the
* `output` object. Returns an empty array if `output` is undefined.
*/
get keys() {
return Object.keys(this?.output ?? {});
}
/**
* Retrieves the values of the `output` object as an array. If the `output`
* object is not defined, it returns an empty array. This getter simplifies
* accessing the values of the parsed data stored in the `output` property,
* allowing for easy iteration over or inspection of the data structure's
* values without direct interaction with the `output` object itself.
*
* @returns {Array<any>} An array containing the values of the `output`
* object. Returns an empty array if `output` is undefined.
*/
get values() {
return Object.values(this?.output ?? {});
}
/**
* Retrieves the entries of the `output` object as an array of [key, value]
* pairs. If the `output` object is not defined, it returns an empty array.
* This getter simplifies accessing both the keys and values of the parsed
* data stored in the `output` property, allowing for easy iteration over
* or inspection of the data structure's entries without direct interaction
* with the `output` object itself.
*
* @returns {Array<[string, any]>} An array containing the entries of the
* `output` object as [key, value] pairs. Returns an empty array if `output`
* is undefined.
*/
get entries() {
return Object.entries(this?.output ?? {});
}
/**
* Custom iterator generator for the MDLSParser instance. This iterator
* allows for easy iteration over the `output` object's entries (key-value
* pairs) using the `for...of` loop or other iterable protocols. If the
* `output` object is not defined or is not an object, the iterator will
* not yield any values, effectively behaving as an iterator over an empty
* collection.
*
* @yields {[string, any]} Yields an array containing a key-value pair
* [key, value] from the `output` object for each iteration.
*/
*[Symbol.iterator]() {
if (!this.output || typeof this.output !== 'object') {
return;
}
for (const entry of Object.entries(this.output)) {
yield entry;
}
}
/**
* A getter for the default string description of the MDLSParser instance.
* This property is used in object-to-string conversions and is accessed
* internally by methods like `Object.prototype.toString`. It simplifies
* identifying the type of the MDLSParser instance in debugging or logging
* scenarios. By default, it returns the name of the constructor, aiding
* in quickly identifying the object's type in console outputs or logs.
*
* @returns {string} The name of the constructor of the instance, which
* helps in identifying the object's type.
*/
get [Symbol.toStringTag]() { return this.constructor.name; }
/**
* Custom inspection function for Node.js `util.inspect` that formats the
* MDLSParser instance's information. This method is invoked when
* `util.inspect` is called on an instance of MDLSParser, providing a
* customized string representation of the instance. It includes the length
* of the `mdlsOutput` and the number of keys in the `output` object.
*
* @param {number} depth The depth to which `util.inspect` will recurse.
* @param {object} options Formatting options passed to `util.inspect`.
* @param {function} inspect Reference to the `util.inspect` function.
* @returns {string} A formatted string representing the MDLSParser instance.
*/
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
const data = inspect({
'mdlsOutput (length)': this.mdlsOutput.length,
'parsed (keys)': Reflect.ownKeys(this.output).length,
}, { ...options, depth: depth - 1 }).replaceAll(/'/g, '');
return `MDLSParser ${data}`;
}
/**
* Extracts a substring from `input` starting at `offset` and enclosed by
* `tokens`. This method is designed to handle nested structures and can
* ignore escaped tokens within strings. It's particularly useful for parsing
* complex data formats or extracting nested information.
*
* @param {string} input The string from which to extract the substring.
* @param {number} offset The position in `input` to start searching for
* the substring.
* @param {[string, string]} tokens An array containing two elements: the
* opening and closing tokens that define the boundaries of the substring.
* @returns {{extracted: string|null, newOffset: number}} An object
* containing the `extracted` substring (null if not found) and `newOffset`,
* the position after the end of the extracted substring. If no valid
* substring is found, `newOffset` will be the same as the input `offset`.
*/
static extractSubstring(input, offset, tokens) {
let [openToken, closeToken] = tokens;
let depth = 0;
let start = -1;
let end = -1;
let escapeNext = false;
let inString = openToken === '"' && closeToken === '"';
for (let i = offset; i < input.length; i++) {
const char = input[i];
if (inString && char === '\\' && !escapeNext) {
escapeNext = true;
continue;
}
if (char === openToken && !escapeNext) {
depth++;
if (start === -1) start = i;
} else if (char === closeToken && !escapeNext) {
depth--;
if (depth === 0) {
end = i;
break;
}
}
escapeNext = false;
}
if (start !== -1 && end !== -1) {
const extracted = inString
? input.slice(start + 1, end)
: input.slice(start, end + 1);
return { extracted, newOffset: end + 1 };
} else {
return { extracted: null, newOffset: offset };
}
}
/**
* Provides a custom inspection symbol for Node.js's `util.inspect` method.
* This enables defining custom object representations in the output of
* `util.inspect`, useful for debugging or logging. When an object includes
* a method keyed by this symbol, it customizes how the object is represented.
*
* @returns {symbol} Symbol for custom inspection in Node.js.
*/
static get nodeInspect() {
return Symbol.for('nodejs.util.inspect.custom');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment