/**
* Creative Developer Tools (CRDT) is a growing suite of tools aimed at script developers and plug-in<br>
* developers for the Adobe Creative Cloud eco-system.<br>
* <br>
* There are two different versions of CRDT: one for UXP/UXPScript and another for ExtendScript.<br>
* <br>
* For downloading and installation info, visit<br>
* <br>
* https://CreativeDeveloperTools.com
* <br>
* <code>crdtuxp</code> contains a number of useful functions. Some of these functions are implemented in JavaScript<br>
* in <code>crdtux.js</code> and are synchronous.<br>
* <br>
* Other functions are delegated to a daemon process, and are always asynchronous.<br>
* <br>
* The list of endpoints is further down<br>
* <br>
* <code>crdtuxp</code> steps out of the UXP security sandbox - which means that as a developer, you need to be<br>
* judicious when using this.<br>
* <br>
* Every solution operates in a unique context. The UXP security measures are helpful in keeping things<br>
* secure, but in many situations, they are a massive overkill.<br>
* <br>
* It should be up to the user/developer/IT department to decide how to handle security.<br>
* <br>
* Sometimes the whole workflow can live inside walled garden, on a disconnected network, without any<br>
* contact with the outside world and not be allowed to run any unvetted software.<br>
* <br>
* Or sometimes the OS security is safe enough for the workflow at hand.<br>
* <br>
* In those cases, the UXP security measures are counter-productive: they represent unnessary hurdles to<br>
* the software development, or make the user interace clunky and user-unfriendly.<br>
* <br>
* Using the UXP sandboxing should be a developer-selectable option, not an enforced requirement, and it<br>
* should be up to the developer and/or the IT department to decide what is appropriate and what not.<br>
*<br>
* @module crdtuxp
* @namespace crdtuxp
*/
const DEFAULT_WAIT_FILE_INTERVAL_MILLISECONDS = 1000;
const DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS = 60000;
const RESPONSE_CHECK_FILE_INTERVAL_MILLISECONDS = 500;
const RESPONSE_CHECK_FILE_TIMEOUT_MILLISECONDS = 1000;
const UXP_VARIANT_PHOTOSHOP_UXP = "UXP_VARIANT_PHOTOSHOP_UXP";
const UXP_VARIANT_PHOTOSHOP_UXPSCRIPT = "UXP_VARIANT_PHOTOSHOP_UXPSCRIPT";
const UXP_VARIANT_INDESIGN_UXP = "UXP_VARIANT_INDESIGN_UXP";
const UXP_VARIANT_INDESIGN_UXPSCRIPT = "UXP_VARIANT_INDESIGN_UXPSCRIPT";
const UXP_VARIANT_INDESIGN_SERVER_UXPSCRIPT = "UXP_VARIANT_INDESIGN_SERVER_UXPSCRIPT";
const FILE_NAME_EXTENSION_JSON = "json";
const FILE_NAME_SUFFIX_TQL_REQUEST = "q";
const FILE_NAME_SUFFIX_TQL_RESPONSE = "r";
const LENGTH_REQUEST_ID = 10;
const RESOLVED_PROMISE_UNDEFINED = Promise.resolve(undefined);
const RESOLVED_PROMISE_FALSE = Promise.resolve(false);
const RESOLVED_PROMISE_TRUE = Promise.resolve(true);
function getPlatformGlobals() {
return global;
}
let platformGlobals = getPlatformGlobals();
platformGlobals.getPlatformGlobals = getPlatformGlobals;
platformGlobals.defineGlobalObject = function defineGlobalObject(globalName) {
if (! platformGlobals[globalName]) {
platformGlobals[globalName] = {};
}
return platformGlobals[globalName];
}
/**
* <code>localhost.tgrg.net</code> resolves to <code>127.0.0.1</code><br>
* <br>
* The Tightener daemon manages the necessary certificate for https
*
* @constant {string} DNS_NAME_FOR_LOCALHOST
*/
const DNS_NAME_FOR_LOCALHOST = "localhost.tgrg.net";
/**
* The Tightener daemon listens for HTTPS connections on port <code>18888</code>.
*
* @constant {number} PORT_TIGHTENER_DAEMON
*/
const PORT_TIGHTENER_DAEMON = 18888;
const LOCALHOST_URL = "https://" + DNS_NAME_FOR_LOCALHOST+ ":" + PORT_TIGHTENER_DAEMON;
/**
* The Tightener daemon provides persistent named scopes (similar to persistent ExtendScript engines).<br>
* <br>
* When executing multiple TQL scripts in succession a named scope will retain any globals that<br>
* were defined by a previous script.
*
* @constant {string} TQL_SCOPE_NAME_DEFAULT
*/
const TQL_SCOPE_NAME_DEFAULT = "defaultScope";
const PLATFORM_MAC_OS_X = "darwin";
if (! module.exports) {
module.exports = {};
}
let crdtuxp = module.exports;
module.exports.IS_MAC = require('os').platform() == PLATFORM_MAC_OS_X;
module.exports.IS_WINDOWS = ! module.exports.IS_MAC;
let FILE_NOT_EXIST_ERROR;
if (module.exports.IS_MAC) {
FILE_NOT_EXIST_ERROR = -2;
}
else {
FILE_NOT_EXIST_ERROR = -4058;
}
/**
* @namespace path
* @memberof crdtuxp
*/
module.exports.path = {};
if (module.exports.IS_MAC) {
module.exports.path.SEPARATOR = "/";
module.exports.path.OTHER_PLATFORM_SEPARATOR = "\\";
}
else {
module.exports.path.SEPARATOR = "\\";
module.exports.path.OTHER_PLATFORM_SEPARATOR = "/";
}
/**
* Setting log level to <code>crdtuxp.LOG_LEVEL_OFF</code> causes all log output to be suppressed.
*
* @constant {number} LOG_LEVEL_OFF
*
*/
const LOG_LEVEL_OFF = 0;
module.exports.LOG_LEVEL_OFF = LOG_LEVEL_OFF;
/**
* Setting log level to <code>crdtuxp.LOG_LEVEL_ERROR</code> causes all log output to be suppressed,<br>
* except for errors.
*
* @constant {number} LOG_LEVEL_ERROR
*/
const LOG_LEVEL_ERROR = 1;
module.exports.LOG_LEVEL_ERROR = LOG_LEVEL_ERROR;
/**
* Setting log level to <code>crdtuxp.LOG_LEVEL_WARNING</code> causes all log output to be suppressed,<br>
* except for errors and warnings.
*
* @constant {number} LOG_LEVEL_WARNING
*/
const LOG_LEVEL_WARNING = 2;
module.exports.LOG_LEVEL_WARNING = LOG_LEVEL_WARNING;
/**
* Setting log level to <code>crdtuxp.LOG_LEVEL_NOTE</code> causes all log output to be suppressed,<br>
* except for errors, warnings and notes.
*
* @constant {number} LOG_LEVEL_NOTE
*/
const LOG_LEVEL_NOTE = 3;
module.exports.LOG_LEVEL_NOTE = LOG_LEVEL_NOTE;
/**
* Setting log level to <code>crdtuxp.LOG_LEVEL_TRACE</code> causes all log output to be output.
*
* @constant {number} LOG_LEVEL_TRACE
*/
const LOG_LEVEL_TRACE = 4;
module.exports.LOG_LEVEL_TRACE = LOG_LEVEL_TRACE;
// Symbolic params to <code>getDir()</code>
/**
* Pass <code>crdtuxp.DESKTOP_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the user's Desktop folder.
*
* @constant {string} DESKTOP_DIR
*/
module.exports.DESKTOP_DIR = "DESKTOP_DIR";
/**
* Pass <code>crdtuxp.DOCUMENTS_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the user's Documents folder.
*
* @constant {string} DOCUMENTS_DIR
*/
module.exports.DOCUMENTS_DIR = "DOCUMENTS_DIR";
/**
* Pass <code>crdtuxp.HOME_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the user's home folder.
*
* @constant {string} HOME_DIR
*/
module.exports.HOME_DIR = "HOME_DIR";
/**
* Pass <code>crdtuxp.LOG_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the Tightener logging folder.
*
* @constant {string} LOG_DIR
*/
module.exports.LOG_DIR = "LOG_DIR";
/**
* Pass <code>crdtuxp.SYSTEMDATA_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the system data folder<br>
* (<code>%PROGRAMDATA%</code> or <code>/Library/Application Support</code>).
*
* @constant {string} SYSTEMDATA_DIR
*/
module.exports.SYSTEMDATA_DIR = "SYSTEMDATA_DIR";
/**
* Pass <code>crdtuxp.TMP_DIR</code> into <code>crdtuxp.getDir()</code> to get the path of the temporary folder.
*
* @constant {string} TMP_DIR
*/
module.exports.TMP_DIR = "TMP_DIR";
/**
* Pass <code>crdtuxp.USERDATA_DIR</code> into <code>crdtuxp.getDir()</code> to get the path to the user data folder<br>
* (<code>%APPDATA%</code> or <code>~/Library/Application Support</code>).
*
* @constant {string} USERDATA_DIR
*/
module.exports.USERDATA_DIR = "USERDATA_DIR";
/**
* <code>crdtuxp.UNIT_NAME_NONE</code> represents unit-less values.
*/
module.exports.UNIT_NAME_NONE = "NONE";
/**
* <code>crdtuxp.UNIT_NAME_INCH</code> for inches.
*/
module.exports.UNIT_NAME_INCH = "\"";
/**
* <code>crdtuxp.UNIT_NAME_CM</code> for centimeters
*/
module.exports.UNIT_NAME_CM = "cm";
/**
* <code>crdtuxp.UNIT_NAME_MM</code> for millimeters
*/
module.exports.UNIT_NAME_MM = "mm";
/**
* <code>crdtuxp.UNIT_NAME_CICERO</code> for ciceros
*/
module.exports.UNIT_NAME_CICERO = "cicero";
/**
* <code>crdtuxp.UNIT_NAME_PICA</code> for picas
*/
module.exports.UNIT_NAME_PICA = "pica";
/**
* <code>crdtuxp.UNIT_NAME_PIXEL</code> for pixels
*/
module.exports.UNIT_NAME_PIXEL = "px";
/**
* <code>crdtuxp.UNIT_NAME_POINT</code> for points
*/
module.exports.UNIT_NAME_POINT = "pt";
// INI parser states
const STATE_IDLE = 0;
const STATE_SEEN_OPEN_SQUARE_BRACKET = 1;
const STATE_SEEN_NON_WHITE = 2;
const STATE_AFTER_NON_WHITE = 3;
const STATE_SEEN_EQUAL = 4;
const STATE_ERROR = 5;
const STATE_SEEN_CLOSE_SQUARE_BRACKET = 6;
const STATE_IN_COMMENT = 7;
// INI value string processing helpers
const REGEXP_TRIM = /^\s*(\S?.*?)\s*$/;
const REGEXP_TRIM_REPLACE = "$1";
const REGEXP_DESPACE = /\s+/g;
const REGEXP_DESPACE_REPLACE = "";
const REGEXP_ALPHA_ONLY = /[^-a-zA-Z0-9$]+/g;
const REGEXP_ALPHA_ONLY_REPLACE = "";
const REGEXP_SECTION_NAME_ONLY = /[^-a-zA-Z0-9$:]+/g;
const REGEXP_SECTION_NAME_ONLY_REPLACE = "";
const REGEXP_NUMBER_ONLY = /^([\d\.]+).*$/;
const REGEXP_NUMBER_ONLY_REPLACE = "$1";
const REGEXP_UNIT_ONLY = /^[\d\.]+\s*(.*)$/;
const REGEXP_UNIT_ONLY_REPLACE = "$1";
const REGEXP_PICAS = /^([\d]+)p(([\d]*)(\.([\d]+)?)?)?$/;
const REGEXP_PICAS_REPLACE = "$1";
const REGEXP_PICAS_POINTS_REPLACE = "$2";
const REGEXP_CICEROS = /^([\d]+)c(([\d]*)(\.([\d]+)?)?)?$/;
const REGEXP_CICEROS_REPLACE = "$1";
const REGEXP_CICEROS_POINTS_REPLACE = "$2";
const LOCALE_EN_US = "en_US";
const DEFAULT_LOCALE = LOCALE_EN_US;
const BTN_OK = "BTN_OK";
const TTL_DIALOG_ALERT = "TTL_DIALOG_ALERT";
let LOCALE_STRINGS = {
BTN_OK: {
"en_US": "OK"
},
TTL_DIALOG_ALERT: {
"en_US": "Alert"
}
};
module.exports.LOCALE = DEFAULT_LOCALE;
module.exports.LOCALE_STRINGS = LOCALE_STRINGS;
module.exports.LOCALE_EN_US = LOCALE_EN_US;
//
// UXP internally caches responses from the server - we need to avoid this as each script
// run can return different results. <code>HTTP_CACHE_BUSTER</code> will be incremented after each use.
//
let HTTP_CACHE_BUSTER = Math.floor(Math.random() * 1000000);
let LOG_LEVEL_STACK = [];
let LOG_ENTRY_EXIT = false;
let LOG_LEVEL = LOG_LEVEL_OFF;
let IN_LOGGER = false;
let LOG_TO_UXPDEVTOOL_CONSOLE = true;
let LOG_TO_CRDT = false;
let LOG_TO_FILE_PATH = undefined;
// Inefficient logging using readSync/writeSync
let SYNC_LOG_TO_FILE_PATH = undefined;
// Set to false to suppress calls to consoleLog
let LOG_TO_CONSOLE = true;
let SYS_INFO;
/**
* Make sure a path ends in a trailing separator (helps identify directory paths)
*
* @function addTrailingSeparator
* @memberof crdtuxp.path
*
* @param {string} filePath - a file path
* @param {string=} separator - the separator to use. If omitted, will try to guess the separator.
* @returns file path with a terminating separator
*/
function addTrailingSeparator(filePath, separator) {
// coderstate: function
let retVal = filePath;
do {
try {
if (! filePath) {
break;
}
const lastChar = filePath.substr(-1);
if (
lastChar == crdtuxp.path.SEPARATOR
||
lastChar == crdtuxp.path.OTHER_PLATFORM_SEPARATOR
) {
break;
}
if (! separator) {
separator = crdtuxp.path.SEPARATOR;
}
retVal += separator;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.path.addTrailingSeparator = addTrailingSeparator;
/**
* Show an alert.
*
* @function alert
* @memberof crdtuxp
*
* @param {string} message - string to display
* @returns {Promise<any>}
*/
function alert(message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
var uxpContext = getUXPContext();
if (uxpContext.uxpVariant == UXP_VARIANT_INDESIGN_SERVER_UXPSCRIPT) {
// We've lost access to the alert() function in InDesign Server, which writes
// to stdout.
// The only workaround I currently have is to pass through ExtendScript
uxpContext.app.doScript(
"alert(" + crdtuxp.dQ(message) + ")",
uxpContext.indesign.ScriptLanguage.JAVASCRIPT);
retVal = RESOLVED_PROMISE_TRUE;
break;
}
if (uxpContext.uxpVariant == UXP_VARIANT_INDESIGN_UXPSCRIPT) {
// InDesign dialogs are not async - they stall the thread until they
// are closed
const dlg = uxpContext.app.dialogs.add();
const col = dlg.dialogColumns.add();
const stText = col.staticTexts.add();
stText.staticLabel = "" + message;
dlg.canCancel = false;
dlg.show();
dlg.destroy();
retVal = RESOLVED_PROMISE_TRUE;
break;
}
if (
uxpContext.uxpVariant == UXP_VARIANT_PHOTOSHOP_UXP
||
uxpContext.uxpVariant == UXP_VARIANT_PHOTOSHOP_UXPSCRIPT
) {
function modalDialog() {
const dlg = document.createElement("dialog");
const frm = document.createElement("form");
const bdy = document.createElement("sp-body");
bdy.textContent = message;
frm.appendChild(bdy);
const buttonContainer = document.createElement("div");
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
frm.appendChild(buttonContainer);
const btnOK = document.createElement("sp-button");
buttonContainer.appendChild(btnOK);
btnOK.textContent = S(BTN_OK);
btnOK.onclick = function onClick() {
dlg.close();
};
dlg.appendChild(frm);
document.body.appendChild(dlg);
return dlg.uxpShowModal(
{
title: S(TTL_DIALOG_ALERT),
resize: "none",
size: { width: 400, height: 100}
}
);
}
function executeAsModalResolveFtn() {
// coderstate: resolver
return true;
};
function executeAsModalRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal =
uxpContext.photoshop.core.executeAsModal(
modalDialog,
{
"commandName": "alert message"
}
).then(
executeAsModalResolveFtn,
executeAsModalRejectFtn);
break;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.alert = alert;
/**
* Decode a string that was encoded using base64.<br>
* <br>
* The evalTQL variant of the function has not been speed-tested; it's mainly for testing things.<br>
* <br>
* I suspect it might only be beneficial for very large long strings, if that.<br>
* The overheads might be larger than the speed benefit.
*
* @function base64decode
* @memberof crdtuxp
*
* @param {string} base64Str - base64 encoded string
* @param {object=} options - options: {<br>
* isBinary: true/false, default false<br>
* }
* @returns {Promise<string|array|undefined>} decoded string
*/
function base64decode(base64Str, options) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let isBinary = options && options.isBinary;
let context = getContext();
if (! context.IS_FORCE_USE_DAEMON) {
let rawString = window.atob(base64Str);
let byteArray = rawStringToByteArray(rawString);
if (byteArray) {
if (isBinary) {
retVal = Promise.resolve(byteArray);
}
else {
retVal = Promise.resolve(binaryUTF8ToStr(byteArray));
}
}
break;
}
let evalTQLOptions = {
isBinary: isBinary
};
let responsePromise =
evalTQL(
"base64decode(" + dQ(base64Str) + ")",
evalTQLOptions
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
if (isBinary) {
retVal = deQuote(response.text);
}
else {
retVal = response.text;
}
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.base64decode = base64decode;
/**
* Encode a string or an array of bytes using Base 64 encoding.<br>
* <br>
* The evalTQL variant of the function has not been speed-tested; it's mainly for testing things.<br>
* <br>
* I suspect it might only be beneficial for very large long strings, if that.<br>
* <br>
* The overheads might be larger than the speed benefit.
*
* @function base64encode
* @memberof crdtuxp
*
* @param {string} s_or_ByteArr - either a string or an array containing bytes (0-255).
* @returns {Promise<string|undefined>} encoded string
*
*/
function base64encode(s_or_ByteArr) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
if (! context.IS_FORCE_USE_DAEMON) {
let byteArray;
if ("string" == typeof s_or_ByteArr) {
byteArray = strToUTF8(s_or_ByteArr);
}
else {
byteArray = s_or_ByteArr;
}
let rawString = byteArrayToRawString(byteArray);
retVal = window.btoa(rawString);
break;
}
const responsePromise =
evalTQL(
"base64encode(" + dQ(s_or_ByteArr) + ")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
try {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.base64encode = base64encode;
/**
* Get the last segment of a path
*
* @function baseName
* @memberof crdtuxp.path
*
* @param {string} filePath - a file path
* @param {string=} separator - the separator to use. If omitted, will try to guess the separator.
* @returns {string} the last segment of the path
*/
function baseName(filePath, separator) {
// coderstate: function
let endSegment;
try {
if (! separator) {
separator = crdtuxp.path.SEPARATOR;
}
// toString() handles cases where filePath is not a real string
let splitPath = filePath.toString().split(separator);
do {
endSegment = splitPath.pop();
}
while (splitPath.length > 0 && endSegment == "");
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return endSegment;
}
module.exports.path.baseName = baseName;
/**
* Decode an array of bytes that contains a UTF-8 encoded string.
*
* @function binaryUTF8ToStr
* @memberof crdtuxp
*
* @param {array} in_byteArray - an array containing bytes (0-255) for a string using UTF-8 encoding.
* @returns {string|undefined} a string or undefined if the UTF-8 is not valid
*/
function binaryUTF8ToStr(in_byteArray) {
// coderstate: function
let retVal = undefined;
try {
let idx = 0;
let len = in_byteArray.length;
let c;
while (idx < len) {
let byte = in_byteArray[idx];
idx++;
let bit7 = byte >> 7;
if (! bit7) {
// U+0000 - U+007F
c = String.fromCharCode(byte);
}
else {
let bit6 = (byte & 0x7F) >> 6;
if (! bit6) {
// Invalid
retVal = undefined;
break;
}
else {
let byte2 = in_byteArray[idx];
idx++;
let bit5 = (byte & 0x3F) >> 5;
if (! bit5) {
// U+0080 - U+07FF
c = String.fromCharCode(((byte & 0x1F) << 6) | (byte2 & 0x3F));
}
else {
let byte3 = in_byteArray[idx];
idx++;
let bit4 = (byte & 0x1F) >> 4;
if (! bit4) {
// U+0800 - U+FFFF
c = String.fromCharCode(
((byte & 0x0F) << 12) |
((byte2 & 0x3F) << 6) |
(byte3 & 0x3F)
);
}
else {
// Not handled U+10000 - U+10FFFF
retVal = undefined;
break;
}
}
}
}
if (! retVal) {
retVal = "";
}
retVal += c;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
retVal = undefined;
}
return retVal;
}
module.exports.binaryUTF8ToStr = binaryUTF8ToStr;
/**
* Make a byte array into a 'fake string'. Not UTF8-aware
*
* @function byteArrayToRawString
* @memberof crdtuxp
*
* @param {array} in_array - a byte array
* @returns {string|undefined} a string with the exact same bytes
*/
function byteArrayToRawString(in_array) {
// coderstate: function
let retVal = "";
try {
for (let idx = 0; idx < in_array.length; idx++) {
retVal += String.fromCharCode(in_array[idx]);
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.byteArrayToRawString = byteArrayToRawString;
/**
* Internal: convert a Unicode character code to a 1 to 3 byte UTF8 byte sequence
*
* @function charCodeToUTF8__
*
* @param {number} in_charCode - a Unicode character code
* @returns {array} an array with 1 to 3 bytes
*/
function charCodeToUTF8__(in_charCode) {
// coderstate: function
let retVal = undefined;
try {
if (in_charCode <= 0x007F) {
retVal = [];
retVal.push(in_charCode);
}
else if (in_charCode <= 0x07FF) {
const hi = 0xC0 + ((in_charCode >> 6) & 0x1F);
const lo = 0x80 + ((in_charCode )& 0x3F);
retVal = [];
retVal.push(hi);
retVal.push(lo);
}
else {
const hi = 0xE0 + ((in_charCode >> 12) & 0x1F);
const mid = 0x80 + ((in_charCode >> 6) & 0x3F);
const lo = 0x80 + ((in_charCode ) & 0x3F);
retVal = [];
retVal.push(hi);
retVal.push(mid);
retVal.push(lo);
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
// anything weird, we return undefined
retVal = undefined;
}
return retVal;
}
/**
* Configure the logger
*
* @function configLogger
* @memberof crdtuxp
*
* @param {object} logInfo - object with logger setup info<br>
* <code>logLevel: 0-4<br>
* logEntryExit: boolean<br>
* logToUXPConsole: boolean<br>
* logToCRDT: boolean<br>
* logToFilePath: undefined or a file path for logging<br></code>
*
* @returns {boolean} success/failure
*/
function configLogger(logInfo) {
// coderstate: function
let retVal = false;
do {
try {
if (! logInfo) {
break;
}
if ("logLevel" in logInfo) {
LOG_LEVEL = logInfo.logLevel;
}
if ("logEntryExit" in logInfo) {
LOG_ENTRY_EXIT = logInfo.logEntryExit;
}
if ("logToUXPConsole" in logInfo) {
LOG_TO_UXPDEVTOOL_CONSOLE = logInfo.logToUXPConsole;
}
if ("logToCRDT" in logInfo) {
LOG_TO_CRDT = logInfo.logToCRDT;
}
if ("logToFilePath" in logInfo) {
LOG_TO_FILE_PATH = logInfo.logToFilePath;
}
if ("syncLogToFilePath" in logInfo) {
SYNC_LOG_TO_FILE_PATH = logInfo.syncLogToFilePath;
}
retVal = true;
}
catch (err) {
consoleLog("configLogger throws " + err);
}
}
while (false);
return retVal;
}
module.exports.configLogger = configLogger;
/**
* Bottleneck for <code>console.log</code>. Only call this function for when <code>crdtuxp.log...</code> is not<br>
* available, e.g. before <code>crdtux</code> has loaded or from within functions used by the logging<br>
* functionaly like <code>crdtuxp.evalTQL()</code><br>
*
* @function consoleLog
* @memberof crdtuxp
*
* @param {...*} args - args for function
*/
function consoleLog(...args) {
// coderstate: function
if (LOG_TO_CONSOLE) {
console.log(...args);
}
if (SYNC_LOG_TO_FILE_PATH) {
fileAppend_(SYNC_LOG_TO_FILE_PATH, args[0] + "\n");
}
}
module.exports.consoleLog = consoleLog;
/**
* Reverse the operation of the <code>crdtuxp.encrypt()</code> function.<br>
* <br>
* Only available to paid developer accounts
*
* @function decrypt
* @memberof crdtuxp
*
* @param {string} s_or_ByteArr - a string or an array of bytes
* @param {string} aesKey - a string or an array of bytes
* @returns {Promise<Array|undefined>} an array of bytes
*/
function decrypt(s_or_ByteArr, aesKey, aesIV) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if (! aesIV) {
aesIV = "";
}
const responsePromise =
evalTQL(
"decrypt(" +
dQ(s_or_ByteArr) + ", " +
dQ(aesKey) + ", " +
dQ(aesIV) +
")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.decrypt = decrypt;
/**
* Delayed execution of a function
*
* @function delayFunction
* @memberof crdtuxp
*
* @param {number} delayTimeMilliseconds - a delay in milliseconds
* @param {function} ftn - a function
* @param {...*} args - optional args for function
* @returns {Promise<any>}
*/
function delayFunction(delayTimeMilliseconds, ftn, ...args) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
try {
function executor(resolveFtn, rejectFtn) {
// coderstate: executor
function timeoutFtn() {
// coderstate: executor
try {
let result = ftn(...args);
if (result instanceof Promise) {
result.then(
resolveFtn,
rejectFtn
);
}
else {
resolveFtn(result);
}
}
catch (err) {
rejectFtn(err);
}
};
setTimeout(
timeoutFtn,
delayTimeMilliseconds
);
};
retVal = new Promise(executor);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.delayFunction = delayFunction;
/**
* Reverse the operation of <code>crdtuxp.dQ()</code> or <code>crdtuxp.sQ()</code>.
*
* @function deQuote
* @memberof crdtuxp
*
* @param {string} quotedString - a quoted string
* @returns {array} a byte array. If the quoted string contains any <code>\uHHHH</code> codes,<br>
* these are first re-encoded using UTF-8 before storing them into the byte array.
*/
function deQuote(quotedString) {
// coderstate: function
let retVal = [];
let state = -1;
let buffer = [];
do {
try {
let qLen = quotedString.length;
if (qLen < 2) {
retVal = strToUTF8(quotedString);
break;
}
const quoteChar = quotedString.charAt(0);
qLen -= 1;
if (quoteChar != quotedString.charAt(qLen)) {
retVal = strToUTF8(quotedString);
break;
}
if (quoteChar != '"' && quoteChar != "'") {
retVal = strToUTF8(quotedString);
break;
}
state = 0;
let cCode = 0;
for (let charIdx = 1; charIdx < qLen; charIdx++) {
if (state == -1) {
break;
}
const c = quotedString.charAt(charIdx);
switch (state) {
case 0:
if (c == '\\') {
state = 1;
}
else {
buffer.push(c.charCodeAt(0));
}
break;
case 1:
if (c == 'x') {
// state 2->3->0
state = 2;
}
else if (c == 'u') {
// state 4->5->6->7->0
state = 4;
}
else if (c == 't') {
buffer.push(0x09);
state = 0;
}
else if (c == 'r') {
buffer.push(0x0D);
state = 0;
}
else if (c == 'n') {
buffer.push(0x0A);
state = 0;
}
else {
buffer.push(c.charCodeAt(0));
state = 0;
}
break;
case 2:
case 4:
if (c >= '0' && c <= '9') {
cCode = c.charCodeAt(0) - 0x30;
state++;
}
else if (c >= 'A' && c <= 'F') {
cCode = c.charCodeAt(0) + 10 - 0x41;
state++;
}
else if (c >= 'a' && c <= 'f') {
cCode = c.charCodeAt(0) + 10 - 0x61;
state++;
}
else {
state = -1;
}
break;
case 3:
case 5:
case 6:
case 7:
if (c >= '0' && c <= '9') {
cCode = (cCode << 4) + c.charCodeAt(0) - 0x30;
}
else if (c >= 'A' && c <= 'F') {
cCode = (cCode << 4) + c.charCodeAt(0) + 10 - 0x41;
}
else if (c >= 'a' && c <= 'f') {
cCode = (cCode << 4) + c.charCodeAt(0) + 10 - 0x61;
}
else {
state = -1;
}
if (state == 3) {
// Done with \xHH
buffer.push(cCode);
state = 0;
}
else if (state == 7) {
// Done with \uHHHHH - convert using UTF-8
let bytes = charCodeToUTF8__(cCode);
if (! bytes) {
state = -1
}
else {
for (let byteIdx = 0; byteIdx < bytes.length; byteIdx++) {
buffer.push(bytes[byteIdx]);
}
state = 0;
}
}
else {
// Next state: 2->3, 4->5->6->7
state++;
}
break;
}
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
if (state == 0) {
retVal = buffer;
}
return retVal;
}
module.exports.deQuote = deQuote;
/**
* Create a directory.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function dirCreate
* @memberof crdtuxp
*
* @param {string} filePath
* @returns {Promise<boolean|undefined>} success or failure
*/
function dirCreate(filePath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
let parentPath = crdtuxp.path.dirName(filePath);
let baseName = crdtuxp.path.baseName(filePath);
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
try {
const stats = uxpContext.fs.lstatSync(parentPath);
if (! stats || ! stats.isDirectory()) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
break;
}
try {
const stats = uxpContext.fs.lstatSync(filePath);
if (stats) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
}
try {
function mkdirResolveFtn() {
// coderstate: resolver
return true;
};
function mkdirRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
// If no callback given, returns a Promise
retVal = uxpContext.fs.mkdir(filePath).then(
mkdirResolveFtn,
mkdirRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
break;
}
const responsePromise =
evalTQL(
"dirCreate(" + dQ(filePath) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.dirCreate = dirCreate;
/**
* Delete a directory.<br>
* <br>
* Not restricted by the UXP security sandbox.<br>
* <br>
* Be very careful with the <code>recurse</code> parameter! It is very easy to delete the wrong directory.
*
* @function dirDelete
* @memberof crdtuxp
*
* @param {string} filePath
* @param {boolean} recurse
* @returns {Promise<boolean|undefined>} success or failure
*/
function dirDelete(filePath, recurse) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
try {
const stats = uxpContext.fs.lstatSync(filePath);
if (! stats || ! stats.isDirectory()) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
retVal = RESOLVED_PROMISE_FALSE;
break;
}
let entries = [];
try {
entries = uxpContext.fs.readdirSync(filePath);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
retVal = RESOLVED_PROMISE_FALSE;
break;
}
if (! recurse) {
if (entries.length > 0) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
try {
function rmdirResolveFtn() {
// coderstate: resolver
return true;
};
function rmdirRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
// If no callback given, returns a Promise
retVal = uxpContext.fs.rmdir(filePath).then(
rmdirResolveFtn,
rmdirRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
retVal = RESOLVED_PROMISE_FALSE;
}
break;
}
const dirPathPrefix = addTrailingSeparator(filePath);
let promises = [];
for (let idx = 0; idx < entries.length; idx++) {
let entryPath = dirPathPrefix + entries[idx];
try {
const stats = uxpContext.fs.lstatSync(entryPath);
if (! stats) {
promises = undefined;
break;
}
let deletePromise;
if (stats.isDirectory()) {
function dirDeleteResolveFtn() {
// coderstate: resolver
return true;
};
function dirDeleteRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
deletePromise = dirDelete(entryPath, true).then(
dirDeleteResolveFtn,
dirDeleteRejectFtn
);
}
else {
function fileDeletResolveFtn() {
// coderstate: resolver
return true;
};
function fileDeleteRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
// If no callback given, returns a Promise
deletePromise = uxpContext.fs.unlink(entryPath).then(
fileDeletResolveFtn,
fileDeleteRejectFtn
);
}
promises.push(deletePromise);
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
promises = undefined;
break;
}
}
if (! promises) {
retVal = RESOLVED_PROMISE_FALSE;
}
function allResolveFtn() {
// coderstate: resolver
try {
function rmdirResolveFtn() {
// coderstate: resolver
return true;
};
function rmdirRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
// If no callback given, returns a Promise
retVal = uxpContext.fs.rmdir(filePath).then(
rmdirResolveFtn,
rmdirRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
retVal = false;
}
};
function allRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
retVal = Promise.allSettled(promises).then(
allResolveFtn,
allRejectFtn
);
break;
}
const responsePromise =
evalTQL(
"dirDelete(" +
dQ(filePath) +
"," +
(recurse ? "true" : "false") +
") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.dirDelete = dirDelete;
/**
* Verify whether a directory exists. Will return <code>false</code> if the path points to a file<br>
* (instead of a directory).<br>
* <br>
* Also see <code>crdtuxp.fileExists()</code>.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function dirExists
* @memberof crdtuxp
*
* @param {string} dirPath - a path to a directory
* @returns {Promise<boolean|undefined>} success or failure
*/
function dirExists(dirPath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
try {
const stats = uxpContext.fs.lstatSync(dirPath);
if (! stats || ! stats.isDirectory()) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
retVal = RESOLVED_PROMISE_FALSE;
break;
}
retVal = RESOLVED_PROMISE_TRUE;
break;
}
const responsePromise =
evalTQL(
"dirExists(" + dQ(dirPath) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.dirExists = dirExists;
/**
* Get the parent directory of a path
*
* @function dirName
* @memberof crdtuxp.path
*
* @param {string} filePath - a file path
* @param {object=} options - options:<br>
* <code>{<br>
* addTrailingSeparator: true/false, default false,<br>
* separator: separatorchar. the separator to use. If omitted, will try to guess the <br>
* separator.<br>
* }</code>
* @param {string=} separator -
* @returns the parent of the path
*/
function dirName(filePath, options) {
// coderstate: function
let retVal;
try {
let separator;
if (options) {
if (options.separator) {
separator = options.separator;
}
}
if (! separator) {
separator = crdtuxp.path.SEPARATOR;
}
// toString() handles cases where filePath is not a real string
let splitPath = filePath.toString().split(separator);
let endSegment;
do {
endSegment = splitPath.pop();
}
while (splitPath.length > 0 && endSegment == "");
retVal = splitPath.join(separator);
if (options && options.addTrailingSeparator) {
retVal += separator;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.path.dirName = dirName;
/**
* Scan a directory.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function dirScan
* @memberof crdtuxp
*
* @param {string} filePath
* @returns {Promise<Array|undefined>} list of items in directory
*/
function dirScan(filePath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
let entries = [];
try {
entries = uxpContext.fs.readdirSync(filePath);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
retVal = RESOLVED_PROMISE_FALSE;
break;
}
retVal = Promise.resolve(entries);
break;
}
const responsePromise =
evalTQL(
"enquote(dirScan(" + dQ(filePath) + ").toString())"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal = undefined;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
const responseText = response.text;
if (! responseText) {
crdtuxp.logError(arguments, "no responseText");
break;
}
const deQuotedResponseText = deQuote(responseText);
if (! deQuotedResponseText) {
crdtuxp.logError(arguments, "no deQuotedResponseText");
break;
}
const binaryResponse = binaryUTF8ToStr(deQuotedResponseText);
if (! binaryResponse) {
crdtuxp.logError(arguments, "no binaryResponse");
break;
}
try {
retVal = JSON.parse(binaryResponse);
}
catch (err) {
crdtuxp.logError(arguments, "failed to parse JSON " + binaryResponse);
break;
}
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.dirScan = dirScan;
/**
* Wrap a string or a byte array into double quotes, encoding any binary data as a string.<br>
* Knows how to handle Unicode characters or binary zeroes.<br>
* <br>
* When the input is a string, high Unicode characters are encoded as <code>\uHHHH</code>.<br>
* <br>
* When the input is a byte array, all bytes are encoded as characters or as <code>\xHH</code><br>
* escape sequences.
*
* @function dQ
* @memberof crdtuxp
*
* @param {string|Array} s_or_ByteArr - a Unicode string or an array of bytes
* @returns {string|undefined} a string enclosed in double quotes. This string is pure 7-bit<br>
* ASCII and can be used into generated script code<br>
* Example:<br>
* <code>let script = "a=b(" + crdtuxp.dQ(somedata) + ");";</code>
*/
function dQ(s_or_ByteArr) {
// coderstate: function
let retVal;
try {
retVal = enQuote__(s_or_ByteArr, "\"");
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.dQ = dQ;
/**
* Encrypt a string or array of bytes using a key. A random salt is added into the mix,<br>
* so even when passing in the same parameter values, the result will be different every time.<br>
* <br>
* Only available to paid developer accounts
*
* @function encrypt
* @memberof crdtuxp
*
* @param {string} s_or_ByteArr - a string or an array of bytes
* @param {string} aesKey - a string or an array of bytes, key
* @param {string=} aesIV - a string or an array of bytes, initial vector
* @returns {Promise<string|undefined>} a base-64 encoded encrypted string.
*/
function encrypt(s_or_ByteArr, aesKey, aesIV) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if (! aesIV) {
aesIV = "";
}
const responsePromise =
evalTQL(
"encrypt(" +
dQ(s_or_ByteArr) + ", " +
dQ(aesKey) + ", " +
dQ(aesIV) +
")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.encrypt = encrypt;
/**
* Internal: Escape and wrap a string in quotes
*
* @function enQuote__
*
* @param {string|Array} s_or_ByteArr - a string or a byte array
* @param {string} quoteChar - a string with a single character to use for quoting (<code>"'"</code> or <code>'"'</code>)
* @returns {string} an string
*/
function enQuote__(s_or_ByteArr, quoteChar) {
// coderstate: function
let retVal = "";
try {
const quoteCharCode = quoteChar.charCodeAt(0);
let isString;
let byteSequence;
if (s_or_ByteArr instanceof ArrayBuffer) {
byteSequence = new Uint8Array(in_byteArray);
}
else {
isString = ("string" == typeof s_or_ByteArr);
byteSequence = s_or_ByteArr;
}
const sLen = byteSequence.length;
let escapedS = "";
for (let charIdx = 0; charIdx < sLen; charIdx++) {
let cCode;
if (isString) {
cCode = byteSequence.charCodeAt(charIdx);
}
else {
cCode = byteSequence[charIdx];
}
if (cCode == 0x5C) {
escapedS += '\\\\';
}
else if (cCode == quoteCharCode) {
escapedS += '\\' + quoteChar;
}
else if (cCode == 0x0A) {
escapedS += '\\n';
}
else if (cCode == 0x0D) {
escapedS += '\\r';
}
else if (cCode == 0x09) {
escapedS += '\\t';
}
else if (cCode < 32 || cCode == 0x7F || (! isString && cCode >= 0x80)) {
escapedS += "\\x" + toHex(cCode, 2);
}
else if (isString && cCode >= 0x80) {
escapedS += "\\u" + toHex(cCode, 4);
}
else {
escapedS += String.fromCharCode(cCode);
}
}
retVal = quoteChar + escapedS + quoteChar;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
/**
* Send a TQL script to the daemon and wait for the result
*
* @function evalTQL
* @memberof crdtuxp
*
* @param {string} tqlScript - a script to run
* @param {object=} options - optional. <br>
* <code>options.wait</code> when <code>false</code> don't wait to resolve, default <code>true</code><br>
* <code>options.isBinary</code> default <code>false</code><br>
* <code>options.tqlScopeName</code> default <code>TQL_SCOPE_NAME_DEFAULT</code><br>
* <code>options.waitFileTimeout</code> default <code>DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS</code><br>
* <code>options.waitFileCheckInterval</code> default <code>DEFAULT_WAIT_FILE_INTERVAL_MILLISECONDS</code><br>
* or can be decoded as a string
* @returns {Promise<any>} a string or a byte array
*/
function evalTQL(tqlScript, options) {
// coderstate: promisor
let retVal = Promise.resolve({ error: true });
do {
try {
let context = crdtuxp.getContext();
let resultIsRawBinary = false;
let wait = true;
let tqlScopeName = TQL_SCOPE_NAME_DEFAULT;
let waitFileTimeout = DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS;
let waitFileCheckInterval = DEFAULT_WAIT_FILE_INTERVAL_MILLISECONDS;
if (options) {
resultIsRawBinary = !!options.isBinary;
wait = options.wait === undefined ? true : options.wait;
tqlScopeName = options.tqlScopeName || TQL_SCOPE_NAME_DEFAULT;
waitFileTimeout = options.waitFileTimeout || DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS;
waitFileCheckInterval = options.waitFileCheckInterval || DEFAULT_WAIT_FILE_INTERVAL_MILLISECONDS;
}
let uxpContext = getUXPContext();
if (! uxpContext.hasNetworkAccess && ! uxpContext.hasDirectFileAccess) {
consoleLog("evalTQL needs direct file access or network access");
// Need either network access or direct file access - cannot do
// without
break;
}
if (! uxpContext.hasNetworkAccess || context.IS_FORCE_DAEMON_FILE_BASED_API) {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
if (! context?.PATH_EVAL_TQL)
{
consoleLog("evalTQL: need context.PATH_EVAL_TQL to be set");
// Need to know where to put the packet
break;
}
if (! uxpContext.tqlRequestID) {
uxpContext.tqlRequestID = 0;
uxpContext.tqlRequestsByID = {};
}
let validUntil =
(new Date()).getTime() + DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS;
let tqlRequest = {
validUntil: validUntil,
wait: wait ? 1 : 0,
sessionID: uxpContext.sessionID,
requestID: ++uxpContext.tqlRequestID,
tqlRequest: tqlScript,
tqlScopeName: tqlScopeName
};
uxpContext.tqlRequestsByID[tqlRequest.requestID] = tqlRequest;
let tqlRequestJSON = JSON.stringify(tqlRequest);
let tqlRequestByteArray = strToUTF8(tqlRequestJSON);
let tqlSharedFilePathPrefix =
context.PATH_EVAL_TQL +
tqlRequest.sessionID +
crdtuxp.leftPad(tqlRequest.requestID, "0", LENGTH_REQUEST_ID);
let requestFilePath =
tqlSharedFilePathPrefix +
FILE_NAME_SUFFIX_TQL_REQUEST +
"." +
FILE_NAME_EXTENSION_JSON;
let responseFilePath =
tqlSharedFilePathPrefix +
FILE_NAME_SUFFIX_TQL_RESPONSE +
"." +
FILE_NAME_EXTENSION_JSON;
function handleResponseData(replyByteArray) {
// coderstate: function
let retVal = undefined;
let responseText;
let responseTextUnwrapped;
do {
try {
if (! replyByteArray) {
break;
}
let jsonResponse = binaryUTF8ToStr(replyByteArray);
if (jsonResponse == "undefined") {
break;
}
let response;
try {
response = JSON.parse(jsonResponse);
}
catch (err) {
break;
}
if (! response || response.result === undefined) {
break;
}
responseText = response.result;
}
catch (err) {
consoleLog("evalTQL handleResponseData throws " + err);
break;
}
}
while (false);
if (resultIsRawBinary) {
responseTextUnwrapped = responseText;
}
else if (responseText) {
responseTextUnwrapped = binaryUTF8ToStr(deQuote(responseText));
}
retVal = {
text: responseTextUnwrapped
};
return retVal;
}
function responseWaitResolveFtn(responseFileState) {
// coderstate: resolver
let retVal = undefined;
do {
delete uxpContext.tqlRequestsByID[tqlRequest.requestID];
if (! responseFileState) {
consoleLog("evalTQL responseWaitResolveFtn timed out waiting for file");
break;
}
let replyByteArray;
function unlinkResolveFtn() {
// coderstate: resolver
return handleResponseData(replyByteArray);
};
function unlinkRejectFtn(reason) {
// coderstate: rejector
consoleLog("evalTQL responseWaitResolveFtn rejected for " + reason);
return handleResponseData(replyByteArray);
};
try {
replyByteArray = new Uint8Array(uxpContext.fs.readFileSync(responseFilePath));
retVal = uxpContext.fs.unlink(responseFilePath).then(
unlinkResolveFtn,
unlinkRejectFtn
);
}
catch (err) {
consoleLog("evalTQL responseWaitResolveFtn throws " + err);
}
}
while (false);
return retVal;
};
function responseWaitRejectFtn(reason) {
// coderstate: rejector
consoleLog("evalTQL responseWaitRejectFtn rejected for " + reason);
delete uxpContext.tqlRequestsByID[tqlRequest.requestID];
return undefined;
};
try {
uxpContext.fs.writeFileSync(requestFilePath, new Uint8Array(tqlRequestByteArray));
}
catch (err) {
consoleLog("evalTQL writeFileSync failed " + err);
break;
}
if (! wait) {
retVal = RESOLVED_PROMISE_UNDEFINED;
break;
}
retVal =
crdtuxp.waitForFile(
responseFilePath,
waitFileCheckInterval,
waitFileTimeout
).then(
responseWaitResolveFtn,
responseWaitRejectFtn
);
break;
}
const init = {
method: "POST",
body: tqlScript
};
const responsePromise =
fetch(LOCALHOST_URL + "/" + tqlScopeName + "?" + HTTP_CACHE_BUSTER, init);
HTTP_CACHE_BUSTER = HTTP_CACHE_BUSTER + 1;
if (! wait) {
retVal = RESOLVED_PROMISE_UNDEFINED;
break;
}
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
const responseTextPromise = response.text();
function responseTextResolveFtn(responseText) {
// coderstate: resolver
let retVal;
do {
let responseTextUnwrapped;
if (! responseText) {
break;
}
if (responseText == "undefined") {
break;
}
try {
if (resultIsRawBinary) {
responseTextUnwrapped = responseText;
}
else {
responseTextUnwrapped = binaryUTF8ToStr(deQuote(responseText));
}
}
catch (err) {
consoleLog("evalTQL responseTextResolveFtn throws " + err);
retVal = {
error: err
};
break;
}
retVal = {
text: responseTextUnwrapped
};
}
while (false);
return retVal;
};
function responseTextRejectFtn(reason) {
// coderstate: rejector
consoleLog("evalTQL responseTextRejectFtn rejected for " + reason);
return {
error: reason
};
};
return responseTextPromise.then(
responseTextResolveFtn,
responseTextRejectFtn
);
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
consoleLog("evalTQL evalTQLRejectFtn rejected for " + reason);
return {
error: reason
};
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
consoleLog("evalTQL throws " + err);
}
}
while (false);
return retVal;
}
module.exports.evalTQL = evalTQL;
/**
* (Internal) Inefficient file append, for debugging use
*
* @function fileAppend_
*
* @param {string} filePath - file to append to
* @param {string} data - string to append
*/
function fileAppend_(filePath, data) {
// coderstate: function
do {
// This is a low-level function. Do not call consoleLog or crdtuxp.logError.
let uxpContext = getUXPContext();
if (! uxpContext.hasDirectFileAccess) {
console.log("fileAppend_ needs hasDirectFileAccess");
break;
}
let fileExists;
try {
const stats = uxpContext.fs.lstatSync(filePath);
fileExists = stats && stats.isFile();
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
console.log("fileAppend_ lstatSync throws " + err);
}
}
let oldData = "";
if (fileExists) {
try {
oldData = binaryUTF8ToStr(new Uint8Array(uxpContext.fs.readFileSync(filePath)));
}
catch (err) {
console.log("fileAppend_ readFileSync throws " + err);
}
}
try {
uxpContext.fs.writeFileSync(filePath, oldData + data);
}
catch (err) {
console.log("fileAppend_ writeFileSync throws " + err);
}
}
while (false);
}
module.exports.fileAppend_ = fileAppend_;
/**
* Append a string to a file (useful for logging).<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileAppendString
* @memberof crdtuxp
*
* @param {string} fileName - path to file
* @param {string} appendStr - data to append. If a newline is needed it needs to be part of appendStr
* @param {object=} options - <code>{<br>
* options.wait = false means don't wait to resolve<br>
* }</code>
* @returns {Promise<boolean|undefined>} success or failure
*/
function fileAppendString(fileName, in_appendStr, options) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
// Don't try direct file access - Photoshop UXPScript lacks an
// append function
let waitForLogConfirmation = true;
if (options && ! options.wait) {
waitForLogConfirmation = false;
}
let evalTQLOptions = {
wait: waitForLogConfirmation
};
const responsePromise = evalTQL(
"var retVal = true;" +
"var handle = fileOpen(" + dQ(fileName) + ",'a');" +
"if (! handle) {" +
"retVal = false;" +
"}" +
"else if (! fileWrite(handle, " + dQ(in_appendStr) + ")) {" +
"retVal = false;" +
"}" +
"if (! fileClose(handle)) {" +
"retVal = false;" +
"}" +
"retVal ? \"true\" : \"false\"",
evalTQLOptions
);
if (options && ! options.wait) {
retVal = RESOLVED_PROMISE_UNDEFINED;
break;
}
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
consoleLog("fileAppendString bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
consoleLog("fileAppendString rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
consoleLog("fileAppendString throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileAppendString = fileAppendString;
/**
* Close a currently open file.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileClose
* @memberof crdtuxp
*
* @param {number} fileHandle - a file handle as returned by <code>crdtuxp.fileOpen()</code>.
* @returns {Promise<boolean|undefined>} success or failure
*/
function fileClose(fileHandle) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
let fileInfo = uxpContext.fileInfoByFileHandle[fileHandle];
if (! fileInfo) {
break;
}
delete uxpContext.fileInfoByFileHandle[fileHandle];
retVal = RESOLVED_PROMISE_TRUE;
break;
}
const responsePromise =
evalTQL(
"fileClose(" + fileHandle + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileClose = fileClose;
/**
* Copy a file.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileCopy
* @memberof crdtuxp
*
* @param {string} fileFromPath - file to copy
* @param {string} fileToPath - where to copy to
* @returns {Promise<boolean|undefined>} success or failure
*/
function fileCopy(fileFromPath, fileToPath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"fileCopy(" + dQ(fileFromPath) + "," + dQ(fileToPath) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileCopy = fileCopy;
/**
* Delete a file.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileDelete
* @memberof crdtuxp
*
* @param {string} filePath
* @returns {Promise<boolean|undefined>} success or failure
*/
function fileDelete(filePath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
try {
const stats = uxpContext.fs.lstatSync(filePath);
if (! stats || ! stats.isFile()) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
break;
}
try {
function unlinkResolveFtn() {
// coderstate: resolver
return true;
};
function unlinkRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return false;
};
// If no callback given, returns a Promise
retVal = uxpContext.fs.unlink(filePath).then(
unlinkResolveFtn,
unlinkRejectFtn
);
}
catch (err) {
}
break;
}
const responsePromise =
evalTQL(
"fileDelete(" + dQ(filePath) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileDelete = fileDelete;
/**
* Check if a file exists. Will return <code>false</code> if the file path points to a directory.<br>
* <br>
* Also see <code>crdtuxp.dirExists()</code>.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileExists
* @memberof crdtuxp
*
* @param {string} filePath
* @returns {Promise<boolean|undefined>} existence of file
*/
function fileExists(filePath) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
try {
const stats = uxpContext.fs.lstatSync(filePath);
if (! stats || ! stats.isFile()) {
retVal = RESOLVED_PROMISE_FALSE;
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
break;
}
retVal = Promise.resolve(true);
break;
}
const responsePromise =
evalTQL(
"fileExists(" + dQ(filePath) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileExists = fileExists;
/**
* Get the file name extension of a path
*
* @function fileNameExtension
* @memberof crdtuxp.path
*
* @param {string} filePath - a file path
* @param {string=} separator - the separator to use. If omitted, will try to guess the separator.
* @returns the lowercased file name extension, without leading period
*/
function fileNameExtension(filePath, separator) {
// coderstate: function
let retVal;
try {
let splitName = crdtuxp.path.baseName(filePath).split(".");
let extension = "";
if (splitName.length > 1) {
extension = splitName.pop();
}
retVal = extension.toLowerCase();
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.path.fileNameExtension = fileNameExtension;
/**
* Open a binary file and return a handle.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileOpen
* @memberof crdtuxp
*
* @param {string} filePath - a native full file path to the file
* @param {string} mode - one of <code>'a'</code>, <code>'r'</code>, <code>'w'</code> (append, read, write)
* @returns {Promise<Number|undefined>} file handle
*/
function fileOpen(filePath, mode) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
let parentPath = crdtuxp.path.dirName(filePath);
let baseName = crdtuxp.path.baseName(filePath);
if (! uxpContext.uniqueFileHandleID) {
uxpContext.uniqueFileHandleID = 0;
uxpContext.fileInfoByFileHandle = {};
}
if (mode == 'w' || mode == 'a') {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
try {
const stats = uxpContext.fs.lstatSync(parentPath);
if (! stats || ! stats.isDirectory()) {
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
break;
}
let uniqueFileHandleID = ++uxpContext.uniqueFileHandleID;
let fileInfo = {
fileHandle: uniqueFileHandleID,
filePath: filePath,
mode: mode
}
uxpContext.fileInfoByFileHandle[uniqueFileHandleID] = fileInfo;
retVal = Promise.resolve(uniqueFileHandleID);
}
else if (mode == 'r') {
try {
const stats = uxpContext.fs.lstatSync(filePath);
if (! stats || ! stats.isFile()) {
break;
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
break;
}
let uniqueFileHandleID = ++uxpContext.uniqueFileHandleID;
let fileInfo = {
fileHandle: uniqueFileHandleID,
filePath: filePath,
mode: mode
}
uxpContext.fileInfoByFileHandle[uniqueFileHandleID] = fileInfo;
retVal = Promise.resolve(uniqueFileHandleID);
}
break;
}
let responsePromise;
if (mode) {
responsePromise =
evalTQL(
"enquote(fileOpen(" + dQ(filePath) + "," + dQ(mode) + "))"
);
}
else {
responsePromise =
evalTQL(
"enquote(fileOpen(" + dQ(filePath) + "))"
);
}
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
try {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
let responseStr = deQuote(response.text);
if (! responseStr) {
crdtuxp.logError(arguments, "no responseStr");
break;
}
let responseData = binaryUTF8ToStr(responseStr);
if (! responseData) {
crdtuxp.logError(arguments, "no responseData");
break;
}
retVal = parseInt(responseData, 10);
if (isNaN(retVal)) {
crdtuxp.logError(arguments, "not a number " + responseData);
retVal = undefined;
break;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
break;
}
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileOpen = fileOpen;
/**
* Read a file into memory.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileRead
* @memberof crdtuxp
*
* @param {number} fileHandle - a file handle as returned by <code>crdtuxp.fileOpen()</code>.
* @param {boolean|object=} options - <code>{ isBinary: true/false, default false }</code>
* @returns {Promise<any>} either a byte array or a string
*/
function fileRead(fileHandle, options) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if ("boolean" == typeof options) {
options = {
isBinary: options
};
}
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
if (! uxpContext.fileInfoByFileHandle) {
break;
}
let fileInfo = uxpContext.fileInfoByFileHandle[fileHandle];
if (! fileInfo) {
break;
}
if (fileInfo.mode != 'r') {
break;
}
let replyByteArray;
try {
replyByteArray = new Uint8Array(uxpContext.fs.readFileSync(fileInfo.filePath));
if (options?.isBinary) {
retVal = Promise.resolve(replyByteArray);
break;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
break;
}
retVal = Promise.resolve(binaryUTF8ToStr(replyByteArray));
break;
}
let evalTQLOptions = {
isBinary: options?.isBinary
};
const responsePromise =
evalTQL(
"enquote(fileRead(" + fileHandle + "))",
evalTQLOptions);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
let byteArrayStr = deQuote(response.text);
if (! byteArrayStr) {
crdtuxp.logError(arguments, "no byteArrayStr");
break;
}
var str = binaryUTF8ToStr(byteArrayStr);
if (! str) {
crdtuxp.logError(arguments, "no str");
break;
}
if (! options?.isBinary) {
retVal = str;
break;
}
retVal = deQuote(str);
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileRead = fileRead;
/**
* Binary write to a file. Strings are written as UTF-8.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function fileWrite
* @memberof crdtuxp
*
* @param {number} fileHandle - a file handle as returned by <code>crdtuxp.fileOpen()</code>.
* @param {string|Array} s_or_ByteArr - data to write to the file
* @returns {Promise<boolean|undefined>} success or failure
*/
function fileWrite(fileHandle, s_or_ByteArr) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
let byteArray;
if ("string" == typeof s_or_ByteArr) {
byteArray = strToUTF8(s_or_ByteArr);
}
else {
byteArray = s_or_ByteArr;
}
let context = getContext();
let uxpContext = getUXPContext();
if (uxpContext.hasDirectFileAccess && ! context.IS_FORCE_USE_DAEMON) {
if (! uxpContext.fileInfoByFileHandle) {
break;
}
let fileInfo = uxpContext.fileInfoByFileHandle[fileHandle];
if (! fileInfo) {
break;
}
if (fileInfo.mode != 'w' && fileInfo.mode != 'a') {
break;
}
let lengthWritten = 0;
try {
lengthWritten = uxpContext.fs.writeFileSync(fileInfo.filePath, new Uint8Array(byteArray));
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
break;
}
retVal = Promise.resolve(lengthWritten == byteArray.length);
break;
}
const responsePromise =
evalTQL(
"fileWrite(" + fileHandle + "," + dQ(byteArray) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.fileWrite = fileWrite;
/**
* Terminate crdtuxp
*
* @function finalize
* @memberof crdtuxp
*/
function finalize() {
// coderstate: promisor
return Promise.finalizePromises();
}
module.exports.finalize = finalize;
/**
* Extract the function name from its arguments
*
* @function functionNameFromArguments
* @memberof crdtuxp
*
* @param {object} functionArguments - pass in the current <code>arguments</code> to the function. This is used to determine the function's name
* @returns {string} function name
*/
function functionNameFromArguments(functionArguments) {
// coderstate: function
let functionName;
try {
functionName = functionArguments.callee.toString().match(/function ([^\(]+)/)[1];
}
catch (err) {
functionName = "[anonymous function]";
}
return functionName;
}
module.exports.functionNameFromArguments = functionNameFromArguments;
/**
* Interpret a value extracted from some INI data as a boolean. Things like <code>y, n, yes, no, true, false, t, f, 0, 1</code><br>
* Default if missing is <code>false</code>
*
* @function getBooleanFromINI
* @memberof crdtuxp
*
* @param {string} in_value - ini value
* @param {string} in_default - value to use if <code>undefined</code> or <code>""</code>
* @returns {boolean} value
*/
function getBooleanFromINI(in_value, in_default) {
// coderstate: function
let retVal = false;
try {
if (! in_value) {
retVal = in_default ? true : false;
}
else {
const value = (in_value + "").replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
if (! value) {
retVal = in_default ? true : false;
}
else {
const firstChar = value.charAt(0).toLowerCase();
const firstValue = parseInt(firstChar, 10);
retVal = firstChar == "y" || firstChar == "t" || (! isNaN(firstValue) && firstValue != 0);
}
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.getBooleanFromINI = getBooleanFromINI;
/**
* Query the daemon to see whether some software is currently activated or not
*
* @function getCapability
* @memberof crdtuxp
*
* @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
* @param {string} capabilityCode - a code for the software features to be activated (as determined by<br>
* the developer who owns the account).<br>
* <code>capabilityCode</code> is not the same as <code>orderProductCode</code> - there can be multiple<br>
* <code>orderProductCode</code> associated with a single <code>capabilityCode</code><br>
* (e.g. <code>capabilityCode</code> 'XYZ', <code>orderProductCode</code> 'XYZ_1YEAR', 'XYZ_2YEAR'...).
* @param {string} encryptionKey - the secret encryption key (created by the developer) needed to decode<br>
* the capability data. You want to make sure this password is obfuscated and contained within encrypted script code.
* @returns {Promise<string|undefined>} a JSON-encoded object with meta object (containing customer GUID, seatIndex,<br>
* decrypted developer-provided data from the activation file).<br>
* The decrypted developer data is embedded as a string, so might be two levels of JSON-encoding to be dealt with <br>
* to get to any JSON-encoded decrypted data
*/
function getCapability(issuer, capabilityCode, encryptionKey) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"getCapability(" +
dQ(issuer) + ", " +
dQ(capabilityCode) + ", " +
dQ(encryptionKey) +
")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (response && response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
if (! response) {
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getCapability = getCapability;
/**
* Determine the license level for CRDT: 0 = not, 1 = basic, 2 = full<br>
* <br>
* Some functions, marked with "Only available to paid developer accounts" <br>
* will only work with level 2. Licensing function only work with level 1<br>
*
* @function getCreativeDeveloperToolsLevel
* @memberof crdtuxp
*
* @returns {Promise<Number|undefined>} - 0, 1 or 2. -1 means: error
*/
function getCreativeDeveloperToolsLevel() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"getCreativeDeveloperToolsLevel()"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = parseInt(response.text, 10);
if (isNaN(retVal)) {
crdtuxp.logError(arguments, "not a number " + response.text);
retVal = undefined;
break;
}
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getCreativeDeveloperToolsLevel = getCreativeDeveloperToolsLevel;
/**
* Get the path of the current script of the caller (<i>not</i> the path of this file).<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function getCurrentScriptPath
* @memberof crdtuxp
*
* @returns {string|undefined} file path of the script file containing the caller of <code>crdtuxp.getCurrentScriptPath()</code>
*/
function getCurrentScriptPath() {
let retVal = undefined;
try {
// Intentionally throw an error to capture the stack trace
throw new Error();
} catch (err) {
// Parse the stack trace to extract the script path
const stackLines = err.stack.split("\n");
// We are interested in the script path for the caller of this function, not the path for crdtuxp.js::getCurrentScriptPath
const NUM_STEPS_FROM_STACK_TOP = 2;
const scriptPathLine = stackLines[NUM_STEPS_FROM_STACK_TOP];
const scriptPathMatch = scriptPathLine.match(/(\/.*?|[A-Za-z]:\\.*?|\\\\.*?\\.*?):\d+:\d+/);
if (scriptPathMatch) {
retVal = scriptPathMatch[1];
}
}
return retVal;
}
module.exports.getCurrentScriptPath = getCurrentScriptPath;
/**
* Get the path of a system directory.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function getDir
* @memberof crdtuxp
*
* @param {string} dirTag - a tag representing the dir:
* <code><br>
* crdtuxp.DESKTOP_DIR<br>
* crdtuxp.DOCUMENTS_DIR<br>
* crdtuxp.HOME_DIR<br>
* crdtuxp.LOG_DIR<br>
* crdtuxp.SYSTEMDATA_DIR<br>
* crdtuxp.TMP_DIR<br>
* crdtuxp.USERDATA_DIR<br>
* </code>
* @returns {Promise<string|undefined>} file path of dir or undefined. Directory paths include a trailing slash or backslash.
*/
function getDir(dirTag) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if (crdtuxp.context) {
if (dirTag == module.exports.DESKTOP_DIR && crdtuxp.context.PATH_DESKTOP) {
retVal = Promise.resolve(crdtuxp.context.PATH_DESKTOP);
break;
}
if (dirTag == module.exports.DOCUMENTS_DIR && crdtuxp.context.PATH_DOCUMENTS) {
retVal = Promise.resolve(crdtuxp.context.PATH_DOCUMENTS);
break;
}
if (dirTag == module.exports.HOME_DIR && crdtuxp.context.PATH_HOME) {
retVal = Promise.resolve(crdtuxp.context.PATH_HOME);
break;
}
if (dirTag == module.exports.LOG_DIR && crdtuxp.context.PATH_LOG) {
retVal = Promise.resolve(crdtuxp.context.PATH_LOG);
break;
}
if (dirTag == module.exports.SYSTEMDATA_DIR && crdtuxp.context.PATH_SYSTEMDATA) {
retVal = Promise.resolve(crdtuxp.context.PATH_SYSTEMDATA);
break;
}
if (dirTag == module.exports.TMP_DIR && crdtuxp.context.PATH_TMP) {
retVal = Promise.resolve(crdtuxp.context.PATH_TMP);
break;
}
if (dirTag == module.exports.USERDATA_DIR && crdtuxp.context.PATH_USERDATA) {
retVal = Promise.resolve(crdtuxp.context.PATH_USERDATA);
break;
}
}
const sysInfoPromise = getSysInfo__();
if (! sysInfoPromise) {
crdtuxp.logError(arguments, "no sysInfoPromise");
break;
}
function evalTQLResolveFtn(sysInfo) {
// coderstate: resolver
let retVal;
do {
if (! sysInfo || sysInfo.error) {
crdtuxp.logError(arguments, "bad response, error = " + sysInfo?.error);
break;
}
if (! (dirTag in sysInfo)) {
crdtuxp.logError(arguments, "unknown tag " + dirTag);
break;
}
retVal = sysInfo[dirTag];
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = sysInfoPromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getDir = getDir;
/**
* Access the environment as available to the daemon program.<br>
* <br>
* Not restricted by the UXP security sandbox.
*
* @function getEnvironment
* @memberof crdtuxp
*
* @param {string} envVarName - name of environment variable
* @returns {Promise<string>} environment variable value
*/
function getEnvironment(envVarName) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"getEnv(" + dQ(envVarName) + ")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getEnvironment = getEnvironment;
/**
* Interpret a string extracted from some INI data as a floating point value, followed by an optional unit<br>
* If there is no unit, then no conversion is performed.
*
* @function getFloatWithUnitFromINI
* @memberof crdtuxp
*
* @param {string} in_valueStr - ini value
* @param {string=} in_convertToUnit - default to use if no match is found
* @returns {number} value
*/
function getFloatWithUnitFromINI(in_valueStr, in_convertToUnit) {
// coderstate: function
let retVal = 0.0;
do {
try {
if (! in_valueStr) {
break;
}
let convertToUnit;
if (in_convertToUnit) {
convertToUnit = in_convertToUnit;
}
else {
convertToUnit = crdtuxp.UNIT_NAME_NONE;
}
let sign = 1.0;
let valueStr = in_valueStr.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE).toLowerCase();
const firstChar = valueStr.charAt(0);
if (firstChar == '-') {
valueStr = valueStr.substring(1);
sign = -1.0;
}
else if (firstChar == '+') {
valueStr = valueStr.substring(1);
}
let picas = undefined;
let ciceros = undefined;
if (valueStr.match(REGEXP_PICAS)) {
picas = parseInt(valueStr.replace(REGEXP_PICAS, REGEXP_PICAS_REPLACE), 10);
valueStr = valueStr.replace(REGEXP_PICAS, REGEXP_PICAS_POINTS_REPLACE);
}
else if (valueStr.match(REGEXP_CICEROS)) {
ciceros = parseInt(valueStr.replace(REGEXP_CICEROS, REGEXP_CICEROS_REPLACE), 10);
valueStr = valueStr.replace(REGEXP_CICEROS, REGEXP_CICEROS_POINTS_REPLACE);
}
const numberOnlyStr = valueStr.replace(REGEXP_NUMBER_ONLY, REGEXP_NUMBER_ONLY_REPLACE);
let numberOnly = parseFloat(numberOnlyStr);
if (isNaN(numberOnly)) {
numberOnly = 0.0;
}
let fromUnit;
if (picas !== undefined) {
fromUnit = crdtuxp.UNIT_NAME_PICA;
numberOnly = picas + numberOnly / 6.0;
}
else if (ciceros !== undefined) {
fromUnit = crdtuxp.UNIT_NAME_CICERO;
numberOnly = ciceros + numberOnly / 6.0;
}
else {
let unitOnly = valueStr.replace(REGEXP_UNIT_ONLY, REGEXP_UNIT_ONLY_REPLACE);
fromUnit = getUnitFromINI(unitOnly, crdtuxp.UNIT_NAME_NONE);
}
let conversion = 1.0;
if (fromUnit != crdtuxp.UNIT_NAME_NONE && convertToUnit != crdtuxp.UNIT_NAME_NONE) {
conversion = unitToInchFactor(fromUnit) / unitToInchFactor(convertToUnit);
}
retVal = sign * numberOnly * conversion;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getFloatWithUnitFromINI = getFloatWithUnitFromINI;
/**
* Interpret a string extracted from some INI data as an array with float values (e.g. <code>"[ 255, 128.2, 1.7]"</code> )
*
* @function getFloatValuesFromINI
* @memberof crdtuxp
*
* @param {string} in_valueStr - ini value
* @returns {array|undefined} array of numbers or undefined
*/
function getFloatValuesFromINI(in_valueStr) {
// coderstate: function
let retVal = undefined;
do {
try {
if (! in_valueStr) {
break;
}
let floatValues = undefined;
const values = in_valueStr.split(",");
for (let idx = 0; idx < values.length; idx++) {
const value = values[idx].replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
let numValue = 0;
if (value) {
numValue = parseFloat(value);
if (isNaN(numValue)) {
floatValues = undefined;
break;
}
}
if (! floatValues) {
floatValues = [];
}
floatValues.push(numValue);
}
retVal = floatValues;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getFloatValuesFromINI = getFloatValuesFromINI;
/**
* Interpret a string extracted from some INI data as an array with int values (e.g. <code>"[ 255, 128, 1]"</code> )
*
* @function getIntValuesFromINI
* @memberof crdtuxp
*
* @param {string} in_valueStr - ini value
* @returns {array|undefined} array of ints or undefined
*/
function getIntValuesFromINI(in_valueStr) {
// coderstate: function
let retVal = undefined;
do {
try {
if (! in_valueStr) {
break;
}
let intValues = undefined;
const values = in_valueStr.split(",");
for (let idx = 0; idx < values.length; idx++) {
const valueStr = values[idx].replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
let value = 0;
if (! valueStr) {
value = 0;
}
else {
value = parseInt(valueStr, 10);
if (isNaN(value)) {
intValues = undefined;
break;
}
}
if (! intValues) {
intValues = [];
}
intValues.push(value);
}
retVal = intValues;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getIntValuesFromINI = getIntValuesFromINI;
/**
* Query the daemon for persisted data<br>
* <br>
* Only available to paid developer accounts<br>
*
* @function getPersistData
* @memberof crdtuxp
*
* @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
* @param {string} attribute - an attribute name for the data
* @param {string} password - the password (created by the developer) needed to decode the persistent data
* @returns {Promise<any>} whatever persistent data is stored for the given attribute
*/
function getPersistData(issuer, attribute, password) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"getPersistData(" +
dQ(issuer) + "," +
dQ(attribute) + "," +
dQ(password) +
")"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getPersistData = getPersistData;
/**
* Get file path to PluginInstaller if it is installed
*
* @function getPluginInstallerPath
* @memberof crdtuxp
*
* @returns {Promise<string>} file path
*/
function getPluginInstallerPath() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"getPluginInstallerPath()"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getPluginInstallerPath = getPluginInstallerPath;
/**
* Internal function. Query the Tightener daemon for system information
*
* @function getSysInfo__
*
* @returns {object} system info
*/
function getSysInfo__() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if (SYS_INFO) {
retVal = Promise.resolve(SYS_INFO);
break;
}
const responsePromise =
evalTQL(
"enquote(sysInfo())"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
let responseWrapperStr = response.text;
if (! responseWrapperStr) {
crdtuxp.logError(arguments, "no response text");
break;
}
let responseData = deQuote(responseWrapperStr);
if (! responseData) {
crdtuxp.logError(arguments, "no responseData");
break;
}
let responseStr = binaryUTF8ToStr(responseData);
if (! responseStr) {
crdtuxp.logError(arguments, "no responseStr");
break;
}
SYS_INFO = JSON.parse(responseStr);
retVal = SYS_INFO;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
/**
* Interpret a string extracted from some INI data as a unit name
*
* @function getUnitFromINI
* @memberof crdtuxp
*
* @param {string} in_value - ini value
* @param {string} in_defaultUnit - default to use if no match is found
* @returns {string} value
*/
function getUnitFromINI(in_value, in_defaultUnit) {
// coderstate: function
const defaultUnit = (in_defaultUnit !== undefined) ? in_defaultUnit : crdtuxp.UNIT_NAME_NONE;
let retVal = defaultUnit;
try {
const value = (in_value + "").replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE).toLowerCase();
if (value == "\"" || value.substring(0,2) == "in") {
retVal = crdtuxp.UNIT_NAME_INCH;
}
else if (value == "cm" || value == "cms" || value.substr(0,4) == "cent") {
retVal = crdtuxp.UNIT_NAME_CM;
}
else if (value == "mm" || value == "mms" || value.substr(0,4) == "mill") {
retVal = crdtuxp.UNIT_NAME_MM;
}
else if (value.substring(0,3) == "cic") {
retVal = crdtuxp.UNIT_NAME_CICERO;
}
else if (value.substring(0,3) == "pic") {
retVal = crdtuxp.UNIT_NAME_PICA;
}
else if (value.substring(0,3) == "pix" || value == "px") {
retVal = crdtuxp.UNIT_NAME_PIXEL;
}
else if (value.substring(0,3) == "poi" || value == "pt") {
retVal = crdtuxp.UNIT_NAME_POINT;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.getUnitFromINI = getUnitFromINI;
/**
* Get our bearings and figure out what operating system context we're operating in.
*
* @function getContext
* @memberof crdtuxp
*
* @returns {object} context
*/
function getContext() {
// coderstate: function
let retVal;
do {
try {
// context can be pre-populated by a calling script, in which case we look if there is any missing
// data that we can add
let context = crdtuxp.context;
if (! context) {
context = {};
crdtuxp.context = context;
}
if (context.areDefaultGetContextPathsInitialized) {
retVal = context;
break;
}
context.areDefaultGetContextPathsInitialized = true;
let uxpContext = getUXPContext();
if (! context.PATH_HOME) {
context.PATH_HOME = uxpContext.os.homedir() + crdtuxp.path.SEPARATOR;
}
if (! context.PATH_DOCUMENTS) {
// This is an educated guess which will be correct most of the time
context.PATH_DOCUMENTS = context.PATH_HOME + "Documents" + crdtuxp.path.SEPARATOR;
}
if (! context.PATH_DESKTOP) {
// This is an educated guess which will be correct most of the time
context.PATH_DESKTOP = context.PATH_HOME + "Desktop" + crdtuxp.path.SEPARATOR;
}
if (! context.PATH_USERDATA) {
// This is an educated guess which will be correct most of the time
if (crdtuxp.IS_MAC) {
context.PATH_USERDATA = context.PATH_HOME + "Library/Application Support/";
}
else {
context.PATH_USERDATA = context.PATH_HOME + "AppData\\Roaming\\";
}
}
if (! context.PATH_TIGHTENER_STORAGE) {
context.PATH_TIGHTENER_STORAGE = context.PATH_USERDATA + "net.tightener" + crdtuxp.path.SEPARATOR;
}
if (! context.PATH_APP_STORAGE) {
context.PATH_APP_STORAGE = context.PATH_TIGHTENER_STORAGE + "Licensing" + crdtuxp.path.SEPARATOR;
}
if (! context.PATH_EVAL_TQL) {
context.PATH_EVAL_TQL = context.PATH_APP_STORAGE + "EvalTQL" + crdtuxp.path.SEPARATOR;
}
retVal = context;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getContext = getContext;
/**
* Get our bearings and figure out what context we're operating in. Depending on the context<br>
* we will or won't have access to certain features
*
* @function getUXPContext
* @memberof crdtuxp
*
* @returns {object} context
*/
function getUXPContext() {
// coderstate: function
let retVal;
do {
try {
if (crdtuxp.uxpContext) {
retVal = crdtuxp.uxpContext;
break;
}
let uxpContext = {};
crdtuxp.uxpContext = uxpContext;
uxpContext.fs = require("fs");
uxpContext.os = require("os");
uxpContext.uxp = require("uxp");
uxpContext.path = require("path");
let storage = uxpContext.uxp.storage;
uxpContext.lfs = storage.localFileSystem;
uxpContext.sessionID = window.crypto.randomUUID().replace(/-/g,"");
try {
// @ts-ignore
uxpContext.indesign = require("indesign");
uxpContext.app = uxpContext.indesign.app;
if (uxpContext.app.name.indexOf("Server") >= 0) {
uxpContext.uxpVariant = UXP_VARIANT_INDESIGN_SERVER_UXPSCRIPT;
uxpContext.hasDirectFileAccess = true;
uxpContext.hasNetworkAccess = true;
}
else {
uxpContext.uxpVariant = UXP_VARIANT_INDESIGN_UXPSCRIPT;
let commandId = uxpContext.uxp?.script?.executionContext?.commandInfo?._manifestCommand?.commandId;
if (commandId == "scriptMainCommand") {
uxpContext.uxpVariant = UXP_VARIANT_INDESIGN_UXPSCRIPT;
uxpContext.hasDirectFileAccess = true;
uxpContext.hasNetworkAccess = true;
}
else {
uxpContext.uxpVariant = UXP_VARIANT_INDESIGN_UXP;
uxpContext.hasDirectFileAccess = false;
uxpContext.hasNetworkAccess = true;
}
}
}
catch (err) {
crdtuxp.logTrace(arguments, "throws " + err);
}
try {
// @ts-ignore
uxpContext.photoshop = require("photoshop");
uxpContext.app = uxpContext.photoshop.app;
let commandId = uxpContext.uxp?.script?.executionContext?.commandInfo?._manifestCommand?.commandId;
if (commandId == "scriptMainCommand") {
uxpContext.uxpVariant = UXP_VARIANT_PHOTOSHOP_UXPSCRIPT;
uxpContext.hasDirectFileAccess = true;
uxpContext.hasNetworkAccess = false;
}
else {
uxpContext.uxpVariant = UXP_VARIANT_PHOTOSHOP_UXP;
uxpContext.hasDirectFileAccess = false;
uxpContext.hasNetworkAccess = true;
}
}
catch (err) {
crdtuxp.logTrace(arguments, "throws " + err);
}
retVal = uxpContext;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.getUXPContext = getUXPContext;
/**
* Hash some text
*
* Not cryptographic
*
* @function hashStringFNV1a
* @memberof crdtuxp
*
* @param {string} s - text to hash
* @returns a hash for the input
*
*/
function hashStringFNV1a(s) {
const fnv1a = (str) => {
let hash = 0x811c9dc5; // 32-bit FNV-1a offset basis
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0; // Ensure 32-bit unsigned
};
var hash = fnv1a(s.toString()).toString(16); // Hexadecimal hash
return hash;
}
module.exports.hashStringFNV1a = hashStringFNV1a;
/**
* Initialize crdtuxp
*
* @function init
* @memberof crdtuxp
*
* @returns {Promise<boolean|undefined>} when true: valid issuer has made CRDT_UXP extra features available
*
*/
function init(context) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
if (! crdtuxp.isProxyPromiseInjected) {
injectProxyPromiseClass();
}
if (! context) {
context = {};
}
if (! crdtuxp.context) {
crdtuxp.context = context;
}
else {
for (attr in context) {
crdtuxp.context[attr] = context[attr];
}
context = crdtuxp.context;
}
// Use crdtuxp.js as a reference point
// FILE_PATH_CRDT_UXP_JS is the path to crdtuxp.js
if (! context.FILE_PATH_CRDT_UXP_JS) {
context.FILE_PATH_CRDT_UXP_JS = crdtuxp.getCurrentScriptPath();
if (! context.FILE_PATH_CRDT_UXP_JS) {
consoleLog("context.FILE_PATH_CRDT_UXP_JS is not defined");
break;
}
}
// FILE_PATH_CRDT_UXP_FOLDER is the folder containing crdtuxp.js
if (! context.FILE_PATH_CRDT_UXP_FOLDER) {
context.FILE_PATH_CRDT_UXP_FOLDER =
crdtuxp.path.dirName(
context.FILE_PATH_CRDT_UXP_JS,
{ addTrailingSeparator: true }
);
if (! context.FILE_PATH_CRDT_UXP_FOLDER) {
consoleLog("context.FILE_PATH_CRDT_UXP_FOLDER is not defined");
break;
}
}
// FILE_PATH_PROJECT_FOLDER is the folder containing the project
if (! context.FILE_PATH_PROJECT_FOLDER) {
if (context.PATH_LAUNCHER_SCRIPT_PARENT) {
context.FILE_PATH_PROJECT_FOLDER = context.PATH_LAUNCHER_SCRIPT_PARENT;
}
else {
context.FILE_PATH_PROJECT_FOLDER =
crdtuxp.path.dirName(
context.FILE_PATH_CRDT_UXP_FOLDER,
{ addTrailingSeparator: true }
);
if (! context.FILE_PATH_PROJECT_FOLDER) {
consoleLog("context.FILE_PATH_PROJECT_FOLDER is not defined");
break;
}
}
}
// RUNPATH_ROOT is the path of the parent folder for the main script file,
// not of the PluginInstaller-generated wrapper launcher script file that
// might live in another folder
//
// FILE_PATH_ROOT is the absolute path that corresponds to RUNPATH_ROOT
//
// RUNPATH_ROOT can be used with require()
//
// FILE_PATH_ROOT can be used for crdtuxp.file... operations; it cannot be used with require()
if (! context.FILE_PATH_ROOT) {
context.FILE_PATH_ROOT = context.FILE_PATH_PROJECT_FOLDER;
}
//
// RUNPATH_ROOT is used with 'require' which in UXP does not accept absolute
// paths
//
if (! context.RUNPATH_ROOT) {
// Use slash separator - it's needed for require()
if (
context.FILE_PATH_ROOT
&&
context.FILE_PATH_PROJECT_FOLDER
&&
context.FILE_PATH_ROOT != context.FILE_PATH_PROJECT_FOLDER
) {
let slashedFilePathRootResolved = context.FILEPath_ROOT.replace(/\\/g,"/");
let slashedFilePathProjectFolder = context.FILEPath_PROJECT_FOLDER.replace(/\\/g,"/");
context.RUNPATH_ROOT = crdtuxp.path.relativeTo(slashedFilePathRootResolved, slashedFilePathProjectFolder, "/")
}
else {
// If the two folder are the same, or one of them is not defined yet,
// use the current folder
context.RUNPATH_ROOT = "./";
}
}
if (! context.RUNPATH_CRDT_UXP) {
context.RUNPATH_CRDT_UXP = context.RUNPATH_ROOT + "/" + crdtuxp.path.baseName(context.FILE_PATH_CRDT_UXP_FOLDER);
}
if (context.ISSUER_GUID && context.ISSUER_EMAIL) {
retVal = crdtuxp.setIssuer(context.ISSUER_GUID, context.ISSUER_EMAIL);
}
else {
retVal = RESOLVED_PROMISE_TRUE;
}
}
catch (err) {
consoleLog("init throws " + err);
}
}
while (false);
return retVal;
}
module.exports.init = init;
/**
* Wrap the system <code>Promise</code> with a new <code>Promise</code> class that allows us to track pending promises
*
* @function injectProxyPromiseClass
* @memberof crdtuxp
*/
function injectProxyPromiseClass() {
// coderstate: procedure
try {
// Save the original Promise class
const SystemPromise = global.Promise;
let PROMISES_PENDING = {};
let LAST_PROMISE_UNIQUE_ID = 0;
crdtuxp.isProxyPromiseInjected = true;
// Define the new Promise class
class Promise {
constructor(executor) {
let curThis = this;
curThis._state = 'pending';
curThis._value = undefined;
curThis._id = ++LAST_PROMISE_UNIQUE_ID;
PROMISES_PENDING[curThis._id] = curThis;
// Create a new instance of the original Promise
this._promise = new SystemPromise((resolve, reject) => {
executor(
(value) => {
curThis._state = 'resolved';
curThis._value = value;
curThis.untracked();
resolve(value);
},
(reason) => {
curThis._state = 'rejected';
curThis._value = reason;
curThis.untracked();
reject(reason);
});
});
}
// Proxy static methods
static pendingPromises() {
// coderstate: function
let retVal = [];
for (let id in PROMISES_PENDING) {
let promise = PROMISES_PENDING[id];
if (promise._state == 'pending') {
retVal.push(promise);
}
}
return retVal;
}
static finalizePromises() {
// coderstate: promisor
function finalizeExecutorFtn(resolveFtn, rejectFtn) {
setImmediate(
() => {
let retVal;
let pendingPromises = Promise.pendingPromises();
if (pendingPromises.length == 0) {
resolveFtn();
}
else {
retVal = Promise.allSettled(pendingPromises).then(
() => {
// coderstate: resolver
return Promise.finalizePromises().
then(resolveFtn, rejectFtn);
},
(reason) => {
// coderstate: rejector
return Promise.finalizePromises().
then(resolveFtn, rejectFtn);
}
);
}
return retVal;
}
);
}
let finalizingPromise = new Promise(finalizeExecutorFtn);
finalizingPromise.untracked();
return finalizingPromise;
}
static resolve(value) {
return new Promise((resolve) => resolve(value));
}
static reject(reason) {
return new Promise((_, reject) => reject(reason));
}
static all(promises) {
return SystemPromise.all(promises);
}
static allSettled(promises) {
return SystemPromise.allSettled(promises)
}
static race(promises) {
return SystemPromise.race(promises);
}
// Proxy instance methods
then(onFulfilled, onRejected) {
return this._promise.then(onFulfilled, onRejected);
}
untracked() {
if (this._id in PROMISES_PENDING) {
delete PROMISES_PENDING[this._id];
}
}
catch(onRejected) {
return this._promise.catch(onRejected);
}
finally(onFinally) {
return this._promise.finally(onFinally);
}
isPending() {
return this._state === 'pending';
}
isResolved() {
return this._state === 'resolved';
}
isRejected() {
return this._state === 'rejected';
}
getValue() {
return this._value;
}
}
global.Promise = Promise;
}
catch (err) {
consoleLog("injectProxyPromiseClass throws " + err);
}
}
module.exports.injectProxyPromiseClass = injectProxyPromiseClass;
/**
* Calculate an integer power of an int value. Avoids using floating point, so should not have any floating-point<br>
* round-off errors. <code>Math.pow()</code> will probably give the exact same result, but I am doubtful that some<br>
* implementations might internally use <code>log</code> and <code>exp</code> to handle <code>Math.pow()</code>
*
* @function intPow
* @memberof crdtuxp
*
* @param {number} i - Integer base
* @param {number} intPower - integer power
* @returns {number|undefined} i ^ intPower
*/
function intPow(i, intPower) {
// coderstate: function
let retVal;
do {
try {
if (Math.floor(intPower) != intPower) {
// Must be integer
retVal = undefined;
break;
}
if (intPower == 0) {
// Handle power of 0: 0^0 is not a number
if (i == 0) {
retVal = NaN;
}
else {
retVal = 1;
}
break;
}
if (intPower > 0 && i == 0) {
retVal = 0;
break;
}
if (intPower < 0 && i == 0) {
retVal = NaN;
break;
}
if (i == 1) {
// Multiplying 1 with itself is 1
retVal = 1;
break;
}
if (intPower == 1) {
// i ^ 1 is i
retVal = i;
break;
}
if (intPower < 0) {
// i^-x is 1/(i^x)
let intermediate = intPow(i, -intPower);
if (intermediate) {
retVal = 1 / intermediate;
}
break;
}
// Divide and conquer
const halfIntPower = intPower >> 1;
const otherHalfIntPower = intPower - halfIntPower;
const part1 = intPow(i, halfIntPower);
if (! part1) {
break;
}
let part2;
if (halfIntPower == otherHalfIntPower) {
part2 = part1;
}
else {
part2 = intPow(i, otherHalfIntPower);
if (! part2) {
break;
}
}
retVal = part1 * part2;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.intPow = intPow;
/**
* Check if the daemon is running.
*
* @function isDaemonResponsive
* @memberof crdtuxp
*
* @returns {Promise<boolean|undefined>} is the Creative Developer Tools daemon responsive?
*/
function isDaemonResponsive() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const options = {
waitFileTimeout: RESPONSE_CHECK_FILE_TIMEOUT_MILLISECONDS,
waitFileCheckInterval: RESPONSE_CHECK_FILE_INTERVAL_MILLISECONDS
};
const isDaemonResponsivePromise =
evalTQL(
"'OK'",
options);
if (! isDaemonResponsivePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
retVal = false;
break;
}
retVal = response.text == "OK";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
consoleLog(arguments, "rejected for " + reason);
return false;
};
retVal = isDaemonResponsivePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
consoleLog(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.isDaemonResponsive = isDaemonResponsive;
/**
* Extend or shorten a string to an exact length, adding <code>padChar</code> as needed
*
* @function leftPad
* @memberof crdtuxp
*
* @param {string} s - string to be extended or shortened
* @param {string} padChar - string to append repeatedly if length needs to extended
* @param {number} len - desired result length
* @returns {string} padded or shortened string
*/
function leftPad(s, padChar, len) {
// coderstate: function
let retVal = "";
do {
try {
retVal = s + "";
if (retVal.length == len) {
break;
}
if (retVal.length > len) {
retVal = retVal.substring(retVal.length - len);
break;
}
const padLength = len - retVal.length;
const padding = new Array(padLength + 1).join(padChar)
retVal = padding + retVal;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.leftPad = leftPad;
/**
* Make a log entry of the call of a function. Pass in the <code>arguments</code> keyword as a parameter.
*
* @function logEntry
* @memberof crdtuxp
*
* @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function. This is used to determine the function's name for the log
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logEntry(reportingFunctionArguments) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_ENTRY_EXIT) {
retVal = logTrace(reportingFunctionArguments, "Entry");
}
return retVal;
}
module.exports.logEntry = logEntry;
/**
* Make a log entry of an error message. Pass in the <code>arguments</code> keyword as the first parameter<br>
* If the error level is below <code>crdtuxp.LOG_LEVEL_ERROR</code> nothing happens
*
* @function logError
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
* This is used to determine the function's name for the log
* @param {any} message - error message
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logError(reportingFunctionArguments, message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_LEVEL >= LOG_LEVEL_ERROR) {
if (! message) {
message = reportingFunctionArguments;
reportingFunctionArguments = undefined;
}
retVal = logMessage(reportingFunctionArguments, LOG_LEVEL_ERROR, message);
}
return retVal;
}
module.exports.logError = logError;
/**
* Make a log entry of the exit of a function. Pass in the <code>arguments</code> keyword as a parameter.
*
* @function logExit
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function. <br>
* This is used to determine the function's name for the log
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logExit(reportingFunctionArguments) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_ENTRY_EXIT) {
retVal = logTrace(reportingFunctionArguments, "Exit");
}
return retVal;
}
module.exports.logExit = logExit;
/**
* Output a log message. Pass in the <code>arguments</code> keyword as the first parameter.
*
* @function logMessage
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
* This is used to determine the function's name for the log
* @param {number} logLevel - log level 0 - 4
* @param {string} message - the note to output
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logMessage(reportingFunctionArguments, logLevel, message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
let savedInLogger = IN_LOGGER;
do {
try {
IN_LOGGER = true;
let functionPrefix = "";
let functionName = "";
if (! message) {
message = reportingFunctionArguments;
reportingFunctionArguments = undefined;
}
else if (reportingFunctionArguments) {
if ("string" == typeof reportingFunctionArguments) {
functionName = reportingFunctionArguments;
}
else {
functionName = functionNameFromArguments(reportingFunctionArguments);
}
functionPrefix += functionName + ": ";
}
let now = new Date();
let timePrefix =
leftPad("" + now.getUTCDate(), "0", 2) +
"-" +
leftPad("" + (now.getUTCMonth() + 1), "0", 2) +
"-" +
leftPad("" + now.getUTCFullYear(), "0", 4) +
" " +
leftPad("" + now.getUTCHours(), "0", 2) +
":" +
leftPad("" + now.getUTCMinutes(), "0", 2) +
":" +
leftPad("" + now.getUTCSeconds(), "0", 2) +
"+00 ";
let platformPrefix = "U ";
let logLevelPrefix = "";
switch (logLevel) {
case LOG_LEVEL_ERROR:
logLevelPrefix = "ERROR";
break;
case LOG_LEVEL_WARNING:
logLevelPrefix = "WARN ";
break;
case LOG_LEVEL_NOTE:
logLevelPrefix = "NOTE ";
break;
case LOG_LEVEL_TRACE:
logLevelPrefix = "TRACE";
break;
default:
logLevelPrefix = " ";
break;
}
let logLine = platformPrefix + timePrefix + "- " + logLevelPrefix + ": " + functionPrefix + message;
let uxpContext = getUXPContext();
if (LOG_TO_UXPDEVTOOL_CONSOLE) {
consoleLog(logLine);
}
else {
if (SYNC_LOG_TO_FILE_PATH) {
fileAppend_(SYNC_LOG_TO_FILE_PATH, logLine + "\n");
}
}
if (savedInLogger) {
// Don't try to use async stuff when nested in logger
break;
}
// Only wait for it to resolve if we have network access
// Otherwise logging slows down to a crawl because of the polling
// mechanism used to communicate with the daemon
let waitForLogConfirmation;
if (uxpContext && uxpContext.hasNetworkAccess) {
waitForLogConfirmation = true;
}
let evalTQLOptions = {
wait: waitForLogConfirmation,
fromLogger: true
};
let promises = [];
if (LOG_TO_CRDT) {
let logCRDTPromise =
evalTQL(
"logMessage(" +
logLevel + "," +
dQ(functionName) + "," +
dQ(message) +
")",
evalTQLOptions
);
if (waitForLogConfirmation) {
promises.push(logCRDTPromise);
}
}
if (LOG_TO_FILE_PATH) {
let appendPromise =
fileAppendString(
LOG_TO_FILE_PATH,
logLine + "\n",
evalTQLOptions);
if (waitForLogConfirmation) {
promises.push(appendPromise);
}
}
if (promises.length) {
retVal = Promise.all(promises);
}
}
catch (err) {
}
}
while (false);
IN_LOGGER = savedInLogger;
return retVal;
}
module.exports.logMessage = logMessage;
/**
* Make a log entry of a note. Pass in the <code>arguments</code> keyword as the first parameter.<br>
* If the error level is below <code>crdtuxp.LOG_LEVEL_NOTE</code> nothing happens
*
* @function logNote
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
* This is used to determine the function's name for the log
* @param {any} message - the note to output
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logNote(reportingFunctionArguments, message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_LEVEL >= LOG_LEVEL_NOTE) {
if (! message) {
message = reportingFunctionArguments;
reportingFunctionArguments = undefined;
}
retVal = logMessage(reportingFunctionArguments, LOG_LEVEL_NOTE, message);
}
return retVal;
}
module.exports.logNote = logNote;
/**
* Emit a trace messsage into the log. Pass in the <code>arguments</code> keyword as the first parameter.<br>
* If the error level is below <code>crdtuxp.LOG_LEVEL_TRACE</code> nothing happens
*
* @function logTrace
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
* This is used to determine the function's name for the log
* @param {any} message - the trace message to output
* @returns {Promise} - a promise that can be used to await the log call completion
*/
function logTrace(reportingFunctionArguments, message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_LEVEL >= LOG_LEVEL_TRACE) {
if (! message) {
message = reportingFunctionArguments;
reportingFunctionArguments = undefined;
}
retVal = logMessage(reportingFunctionArguments, LOG_LEVEL_TRACE, message);
}
return retVal;
}
module.exports.logTrace = logTrace;
/**
* Emit a warning messsage into the log. Pass in the <code>arguments</code> keyword as the first parameter.<br>
* If the error level is below <code>crdtuxp.LOG_LEVEL_WARNING</code> nothing happens
*
* @function logWarning
* @memberof crdtuxp
*
* @param {any} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
* This is used to determine the function's name for the log
* @param {any} message - the warning message to output
*/
function logWarning(reportingFunctionArguments, message) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
if (LOG_LEVEL >= LOG_LEVEL_WARNING) {
if (! message) {
message = reportingFunctionArguments;
reportingFunctionArguments = undefined;
}
retVal = logMessage(reportingFunctionArguments, LOG_LEVEL_WARNING, message);
}
return retVal;
}
module.exports.logWarning = logWarning;
/**
* The unique <code>GUID</code> of this computer<br>
* <br>
* Only available to paid developer accounts
*
* @function machineGUID
* @memberof crdtuxp
*
* @returns {Promise<string | undefined>} a <code>GUID</code> string
*/
function machineGUID() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"machineGUID()"
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text;
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.machineGUID = machineGUID;
/**
* Launch the PluginInstaller if it is installed and configured
*
* @function pluginInstaller
* @memberof crdtuxp
*
* @returns {Promise<boolean|undefined>} success or failure
*/
function pluginInstaller() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"pluginInstaller() ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// coderstate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.pluginInstaller = pluginInstaller;
/**
* Restore the log level to what it was when pushLogLevel was called
*
* @function popLogLevel
* @memberof crdtuxp
*
* @returns {number} log level that was popped off the stack
*/
function popLogLevel() {
// coderstate: function
let retVal;
retVal = LOG_LEVEL;
if (LOG_LEVEL_STACK.length > 0) {
LOG_LEVEL = LOG_LEVEL_STACK.pop();
}
else {
LOG_LEVEL = LOG_LEVEL_OFF;
}
return retVal;
}
module.exports.popLogLevel = popLogLevel;
/**
* Convert a callback-based function into a Promise
*
* @function promisify
* @memberof crdtuxp
*
* @param {function} ftn - callback-based function
* @returns {function} promisified function
*/
function promisify(ftn) {
// coderstate: function
return (...args) => {
return new Promise(
function executorFtn(resolveFtn, rejectFtn) {
// coderstate: executor
args.push(
(err, ...results) => {
if (err) {
return rejectFtn(err)
}
return resolveFtn(results.length === 1 ? results[0] : results);
}
);
ftn.call(this, ...args)
})
}
}
module.exports.promisify = promisify;
/**
* Convert a callback-based member function into a Promise
*
* @function promisifyWithContext
* @memberof crdtuxp
*
* @param {function} ftn - callback-based function
* @param {object} context - A context for the callback
* @returns {function} promisified function
*/
function promisifyWithContext(ftn, context) {
// coderstate: function
return (...args) => {
return new Promise(
function executorFtn(resolveFtn, rejectFtn) {
// coderstate: executor
args.push(
(err, ...results) => {
if (err) {
return rejectFtn(err)
}
return resolveFtn(results.length === 1 ? results[0] : results);
}
);
ftn.call(context, ...args)
})
}
}
module.exports.promisifyWithContext = promisifyWithContext;
/**
* Save the previous log level and set a new log level
*
* @function pushLogLevel
* @memberof crdtuxp
*
* @param {number} newLogLevel - new log level to set
* @returns {number} previous log level
*/
function pushLogLevel(newLogLevel) {
// coderstate: function
let retVal;
retVal = LOG_LEVEL;
LOG_LEVEL_STACK.push(LOG_LEVEL);
LOG_LEVEL = newLogLevel;
return retVal;
}
module.exports.pushLogLevel = pushLogLevel;
/**
* Make a 'fake string' into a byte array. Not UTF8-aware
*
* @function rawStringToByteArray
* @memberof crdtuxp
*
* @param {string} in_str - a raw string (possibly invalid UTF-8)
* @returns {array|undefined} an array of bytes
*/
function rawStringToByteArray(in_str) {
// coderstate: function
let retVal = [];
try {
for (let idx = 0; idx < in_str.length; idx++) {
let c = in_str.charCodeAt(idx);
if (c > 255) {
retVal = undefined;
break;
}
retVal.push(c);
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.rawStringToByteArray = rawStringToByteArray;
/**
* Read a bunch of text and try to extract structured information in .INI format<br>
* <br>
* This function is lenient and is able to extract slightly mangled INI data from the text frame<br>
* content of an InDesign text frame.<br>
* <br>
* This function knows how to handle curly quotes should they be present.<br>
* <br>
* The following flexibilities have been built-in:<br>
* <br>
* - Attribute names are case-insensitive and anything not <code>a-z 0-9</code> is ignored.<br>
* Entries like <code>this or that = ...</code> or <code>thisOrThat = ...</code> or <code>this'orThat = ...</code> are<br>
* all equivalent. Only letters and digits are retained, and converted to lowercase.<br>
* <br>
* - Attribute values can be quoted with either single, double, curly quotes.<br>
* This often occurs because InDesign can be configured to convert normal quotes into<br>
* curly quotes automatically.<br>
* Attribute values without quotes are trimmed (e.g. <code>bla = x </code> is the same as <code>bla=x</code>)<br>
* Spaces are retained in quoted attribute values.<br>
* <br>
* - Any text will be ignore if not properly formatted as either a section name or an attribute-value<br>
* pair with an equal sign<br>
* <br>
* - Hard and soft returns are equivalent<br>
* <br>
* The return value is an object with the section names at the top level, and attribute names<br>
* below that. The following .INI<br>
* <code><br>
* [My data]<br>
* this is = " abc "<br>
* that = abc<br>
* </code><br>
* returns<br>
* <code><br>
* {<br>
* "mydata": {<br>
* "__rawSectionName": "My data",<br>
* "thisis": " abc ",<br>
* "that": "abc"<br>
* }<br>
* }<br>
* </code><br>
* Duplicated sections and entries are automatically suffixed with a counter suffix - e.g.<br>
* <code><br>
* [main]<br>
* a=1<br>
* a=2<br>
* a=3<br>
* </code><br>
* is equivalent with<br>
* <code><br>
* [main]<br>
* a=1<br>
* a_2=2<br>
* a_3=3<br>
* </code><br>
* and<br>
* <code><br>
* [a]<br>
* a=1<br>
* [a]<br>
* a=2<br>
* </code><br>
* is equivalent with
* <code><br>
* [a]<br>
* a=1<br>
* [a_2]<br>
* a=2<br>
* </code><br>
*
* @function readINI
* @memberof crdtuxp
*
* @param {string} in_text - raw text, which might or might not contain some INI-formatted data mixed with normal text
* @returns {object} either the ini data or <code>undefined</code>.
*/
function readINI(in_text) {
// coderstate: function
let retVal = undefined;
do {
try {
if (! in_text) {
break;
}
if ("string" != typeof in_text) {
break;
}
let text = in_text + "\r";
let state = STATE_IDLE;
let attr = "";
let value = "";
let attrSpaceCount = 0;
let rawSectionName = "";
let sectionName = "";
let section = undefined;
let attrCounters = {};
let sectionCounters = {};
for (let idx = 0; state != STATE_ERROR && idx < text.length; idx++) {
const c = text.charAt(idx);
switch (state) {
default:
state = STATE_ERROR;
break;
case STATE_IDLE:
if (c == '[') {
state = STATE_SEEN_OPEN_SQUARE_BRACKET;
rawSectionName = "";
}
else if (c == '#') {
state = STATE_IN_COMMENT;
}
else if (c > ' ') {
attr = c;
attrSpaceCount = 0;
state = STATE_SEEN_NON_WHITE;
}
break;
case STATE_IN_COMMENT:
case STATE_SEEN_CLOSE_SQUARE_BRACKET:
if (c == '\r' || c == '\n') {
state = STATE_IDLE;
}
break;
case STATE_SEEN_OPEN_SQUARE_BRACKET:
if (c == ']') {
state = STATE_SEEN_CLOSE_SQUARE_BRACKET;
sectionName = rawSectionName.toLowerCase();
sectionName = sectionName.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE);
sectionName = sectionName.replace(REGEXP_SECTION_NAME_ONLY, REGEXP_SECTION_NAME_ONLY_REPLACE);
if (sectionName) {
if (! retVal) {
retVal = {};
}
let sectionSuffix = "";
let sectionCounter = 1;
if (sectionName in sectionCounters) {
sectionCounter = sectionCounters[sectionName];
sectionCounter++;
sectionSuffix = "_" + sectionCounter;
}
sectionCounters[sectionName] = sectionCounter;
sectionName += sectionSuffix;
section = {};
retVal[sectionName] = section;
// @ts-ignore
section.__rawSectionName = rawSectionName;
attrCounters = {};
}
}
else {
rawSectionName += c;
}
break;
case STATE_SEEN_NON_WHITE:
if (c == "=") {
value = "";
state = STATE_SEEN_EQUAL;
}
else if (c == '\r' || c == '\n') {
state = STATE_IDLE;
}
else if (c != " ") {
while (attrSpaceCount > 0) {
attr += " ";
attrSpaceCount--;
}
attr += c;
}
else {
attrSpaceCount++;
}
break;
case STATE_SEEN_EQUAL:
if (c != '\r' && c != '\n') {
value += c;
}
else {
value = value.replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
if (value.length >= 2) {
let firstChar = value.charAt(0);
let lastChar = value.charAt(value.length - 1);
if (
(firstChar == "\"" || firstChar == "“" || firstChar == "”")
&&
(lastChar == "\"" || lastChar == "“" || lastChar == "”")
) {
value = value.substring(1, value.length - 1);
}
else if (
(firstChar == "'" || firstChar == "‘" || firstChar == "’")
&&
(lastChar == "'" || lastChar == "‘" || lastChar == "’")
) {
value = value.substring(1, value.length - 1);
}
}
if (section) {
attr = attr.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE).toLowerCase();
attr = attr.replace(REGEXP_ALPHA_ONLY, REGEXP_ALPHA_ONLY_REPLACE);
if (attr) {
let attrSuffix = "";
let attrCounter = 1;
if (attr in attrCounters) {
attrCounter = attrCounters[attr];
attrCounter++;
attrSuffix = "_" + attrCounter;
}
attrCounters[attr] = attrCounter;
attr += attrSuffix;
section[attr] = value;
}
}
state = STATE_IDLE;
}
break;
}
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.readINI = readINI;
/**
* Calculate the relative path to go from a root path to a target path
*
* @function relativeTo
* @memberof crdtuxp.path
*
* @param {string} rootPath - the base path to relate to
* @param {string} targetPath - the base path to reach from the root path
* @param {string=} pathSeparator - optional: the separator to use. Defaults to the system separator
* @returns {string|undefined} relative path
*/
function relativeTo(rootPath, targetPath, pathSeparator) {
// coderstate: function
let retVal = undefined;
do {
try {
if (! pathSeparator) {
pathSeparator = module.exports.path.SEPARATOR
}
let splitRootPath = rootPath.split(pathSeparator);
let splitTargetPath = targetPath.split(pathSeparator);
let relativePath = "";
let rootIdx = 0;
let targetIdx = 0;
while (rootIdx < splitRootPath.length || targetIdx < splitTargetPath.length) {
// Skip empty segments
let rootSegment = undefined;
while (! rootSegment && rootIdx < splitRootPath.length) {
rootSegment = splitRootPath[rootIdx];
rootIdx++;
}
let targetSegment = undefined;
while (! targetSegment && targetIdx < splitTargetPath.length) {
targetSegment = splitTargetPath[targetIdx];
targetIdx++;
}
if (rootSegment && targetSegment) {
if (rootSegment != targetSegment) {
if (! relativePath) {
relativePath = ".." + pathSeparator + targetSegment;
}
else {
relativePath = ".." + pathSeparator + relativePath + pathSeparator + targetSegment;
}
}
}
else if (rootSegment) {
relativePath = ".." + pathSeparator + relativePath;
}
else if (targetSegment) {
relativePath = relativePath + pathSeparator + targetSegment;
}
}
retVal = relativePath;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.path.relativeTo = relativeTo;
/**
* Extend or shorten a string to an exact length, adding <code>padChar</code> as needed
*
* @function rightPad
* @memberof crdtuxp
*
* @param {string} s - string to be extended or shortened
* @param {string} padChar - string to append repeatedly if length needs to extended
* @param {number} len - desired result length
* @returns {string} padded or shortened string
*/
function rightPad(s, padChar, len) {
// coderstate: function
let retVal = "";
do {
try {
retVal = s + "";
if (retVal.length == len) {
break;
}
if (retVal.length > len) {
retVal = retVal.substring(0, len);
break;
}
const padLength = len - retVal.length;
const padding = new Array(padLength + 1).join(padChar)
retVal = retVal + padding;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.rightPad = rightPad;
/**
* Fetch a localized string.
*
* @function S
* @memberof crdtuxp
*
* @param {string} stringCode - a token for the string to be localized (e.g. BTN_OK)
* @param {string=} locale - a locale. Optional - defaults to "en_US"
* @returns {string} a localized string. If the stringCode is not found, returns the stringCode itself.
*/
function S(stringCode, locale) {
// coderstate: function
let retVal = stringCode;
do {
try {
if (! locale) {
locale = DEFAULT_LOCALE;
}
if (! (stringCode in LOCALE_STRINGS)) {
break;
}
const localeStrings = LOCALE_STRINGS[stringCode];
if (locale in localeStrings) {
retVal = localeStrings[locale];
}
else if (LOCALE_EN_US in localeStrings) {
retVal = localeStrings[LOCALE_EN_US];
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.S = S;
/**
* Send in activation data so the daemon can determine whether some software is currently activated or not.<br>
* <br>
* Needs to be followed by a <code>crdtuxp.sublicense()</code> call
*
* @function setIssuer
* @memberof crdtuxp
*
* @param {string} issuerGUID - a GUID identifier for the developer account as seen in the PluginInstaller
* @param {string} issuerEmail - the email for the developer account as seen in the PluginInstaller
* @returns {Promise<boolean|undefined>} - success or failure
*/
function setIssuer(issuerGUID, issuerEmail) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"setIssuer(" + dQ(issuerGUID) + "," + dQ(issuerEmail) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// codestate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.setIssuer = setIssuer;
/**
* Wrap a string or a byte array into single quotes, encoding any binary data as a string.<br>
* Knows how to handle Unicode characters or binary zeroes.<br>
* <br>
* When the input is a string, high Unicode characters are encoded as <code>\uHHHH</code><br>
* <br>
* When the input is a byte array, all bytes are encoded as <code>\xHH</code> escape sequences.<br>
*
* @function sQ
* @memberof crdtuxp
*
* @param {string} s_or_ByteArr - a Unicode string or an array of bytes
* @returns {string} a string enclosed in double quotes. This string is pure 7-bit ASCII and can<br>
* be used into generated script code<br>
* Example:<br>
* <code>let script = "a=b(" + crdtuxp.sQ(somedata) + ");";</code>
*/
function sQ(s_or_ByteArr) {
// coderstate: function
let retVal;
try {
retVal = enQuote__(s_or_ByteArr, "'");
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.sQ = sQ;
/**
* Store some persistent data (e.g. a time stamp to determine a demo version lapsing)<br>
* <br>
* Only available to paid developer accounts
*
* @function setPersistData
* @memberof crdtuxp
*
* @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
* @param {string} attribute - an attribute name for the data
* @param {string} password - the password (created by the developer) needed to decode the persistent data
* @param {string} data - any data to persist
* @returns {Promise<boolean|undefined>} success or failure
*/
function setPersistData(issuer, attribute, password, data) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"setPersistData(" +
dQ(issuer) + "," +
dQ(attribute) + "," +
dQ(password) + "," +
dQ(data) +
") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// codestate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.setPersistData = setPersistData;
/**
* Remove a trailing separator, if any, from a path
*
* @function stripTrailingSeparator
* @memberof crdtuxp
*
* @param {string} filePath - a file path
* @param {string=} separator - the separator to use. If omitted, will try to guess the separator.
* @returns the file path without trailing separator
*/
function stripTrailingSeparator(filePath, separator) {
// coderstate: function
let retVal = filePath;
do {
try {
if (! filePath) {
break;
}
const lastChar = filePath.substr(-1);
if (! separator) {
if (lastChar == crdtuxp.path.SEPARATOR || lastChar == crdtuxp.path.OTHER_PLATFORM_SEPARATOR) {
retVal = filePath.substr(0, filePath.length - 1);
}
}
else {
if (lastChar == separator) {
retVal = filePath.substr(0, filePath.length - 1);
}
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.stripTrailingSeparator = stripTrailingSeparator;
/**
* Encode a string into an byte array using UTF-8
*
* @function strToUTF8
* @memberof crdtuxp
*
* @param {string} in_s - a string
* @returns {array|undefined} a byte array
*/
function strToUTF8(in_s) {
// coderstate: function
let retVal = undefined;
try {
let idx = 0;
let len = in_s.length;
let cCode;
while (idx < len) {
cCode = in_s.charCodeAt(idx);
idx++;
let bytes = charCodeToUTF8__(cCode);
if (! bytes) {
retVal = undefined;
break;
}
else {
for (let byteIdx = 0; byteIdx < bytes.length; byteIdx++) {
if (! retVal) {
retVal = [];
}
retVal.push(bytes[byteIdx]);
}
}
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.strToUTF8 = strToUTF8;
/**
* Send in sublicense info generated in the PluginInstaller so the daemon can determine whether some<br>
* software is currently activated or not.<br>
* <br>
* Needs to be preceded by a <code>crdtuxp.setIssuer()</code> call.
*
* @function sublicense
* @memberof crdtuxp
*
* @param {string} key - key needed to decode activation data
* @param {string} activation - encrypted activation data
* @returns { Promise<boolean|undefined> } success or failure
*/
function sublicense(key, activation) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
const responsePromise =
evalTQL(
"sublicense(" + dQ(key) + "," + dQ(activation) + ") ? \"true\" : \"false\""
);
if (! responsePromise) {
break;
}
function evalTQLResolveFtn(response) {
// codestate: resolver
let retVal;
do {
if (! response || response.error) {
crdtuxp.logError(arguments, "bad response, error = " + response?.error);
break;
}
retVal = response.text == "true";
}
while (false);
return retVal;
};
function evalTQLRejectFtn(reason) {
// coderstate: rejector
crdtuxp.logError(arguments, "rejected for " + reason);
return undefined;
};
retVal = responsePromise.then(
evalTQLResolveFtn,
evalTQLRejectFtn
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.sublicense = sublicense;
/**
* Test if we can access the path-based file I/O APIs
*
* @function testDirectFileAccess
* @memberof crdtuxp
*
* @returns {boolean} whether APIs are accessible
*/
function testDirectFileAccess() {
// coderstate: function
let retVal = false;
do {
try {
let uxpContext = crdtuxp.getUXPContext();
if (! uxpContext) {
crdtuxp.logError(arguments, "failed to get uxpContext");
break;
}
let homeDir = uxpContext.os.homedir();
if (! homeDir) {
crdtuxp.logError(arguments, "failed to os.homedir()");
break;
}
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
const stats = uxpContext.fs.lstatSync(homeDir);
if (stats && stats.isDirectory()) {
retVal = true;
}
}
catch (err) {
}
}
while (false);
return retVal;
}
module.exports.testDirectFileAccess = testDirectFileAccess;
/**
* Test if we can access the network APIs
*
* @function testNetworkAccess
* @memberof crdtuxp
*
* @returns {Promise<boolean|undefined>} whether APIs are accessible
*/
function testNetworkAccess() {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_FALSE;
try {
let context = crdtuxp.getContext();
let savedForceUseDaemon = context.IS_FORCE_USE_DAEMON;
let savedForceDaemonFileBasedAPI = context.IS_FORCE_DAEMON_FILE_BASED_API;
let uxpContext = crdtuxp.getUXPContext();
let savedHasNetworkAccess = uxpContext.hasNetworkAccess;
context.IS_FORCE_USE_DAEMON = true;
context.IS_FORCE_DAEMON_FILE_BASED_API = false;
uxpContext.hasNetworkAccess = true;
try {
return crdtuxp.base64encode("abc").then(
function blowPipesResolveFtn(result) {
return result == "YWJj";
}
).catch(
function blowPipesRejectFtn(reason) {
return false;
}
);
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
uxpContext.hasNetworkAccess = savedHasNetworkAccess;
context.IS_FORCE_USE_DAEMON = savedForceUseDaemon;
context.IS_FORCE_DAEMON_FILE_BASED_API = savedForceDaemonFileBasedAPI;
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.testNetworkAccess = testNetworkAccess;
/**
* Convert an integer into a hex representation with a fixed number of digits.<br>
* Negative numbers are converted using 2-s complement (so <code>-15</code> results in <code>0x01</code>)
*
* @function toHex
* @memberof crdtuxp
*
* @param {number} i - integer to convert to hex
* @param {number} numDigits - How many digits. Defaults to 4 if omitted.
* @returns { string } hex-encoded integer
*/
let TO_HEX_BUNCH_OF_ZEROES = "";
function toHex(i, numDigits) {
// coderstate: function
let retVal = "";
do {
try {
if (! numDigits) {
numDigits = 4;
}
if (i < 0) {
let upper = intPow(2, numDigits*4);
if (! upper) {
break;
}
// Calculate 2's complement with numDigits if negative
i = (upper + i) & (upper - 1);
}
// Calculate and cache a long enough string of zeroes
let zeroes = TO_HEX_BUNCH_OF_ZEROES;
if (! zeroes) {
zeroes = "0";
}
if (zeroes.length < numDigits) {
while (zeroes.length < numDigits) {
zeroes += zeroes;
}
TO_HEX_BUNCH_OF_ZEROES = zeroes;
}
retVal = i.toString(16).toLowerCase(); // Probably always lowercase by default, but just in case...
if (retVal.length > numDigits) {
retVal = retVal.substring(retVal.length - numDigits);
}
else if (retVal.length < numDigits) {
retVal = zeroes.substring(0, numDigits - retVal.length) + retVal;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.toHex = toHex;
/**
* Conversion factor from a length unit into inches
*
* @function unitToInchFactor
* @memberof crdtuxp
*
* @param {string} in_unit - unit name (<code>crdtuxp.UNIT_NAME...</code>)
* @returns {number} conversion factor or 1.0 if unknown/not applicable
*/
function unitToInchFactor(in_unit) {
// coderstate: function
let retVal = 1.0;
try {
switch (in_unit) {
case crdtuxp.UNIT_NAME_CM:
retVal = 1.0/2.54;
break;
case crdtuxp.UNIT_NAME_MM:
retVal = 1.0/25.4;
break;
case crdtuxp.UNIT_NAME_CICERO:
retVal = 0.17762;
break;
case crdtuxp.UNIT_NAME_PICA:
retVal = 1.0/12.0;
break;
case crdtuxp.UNIT_NAME_PIXEL:
retVal = 1.0/72.0;
break;
case crdtuxp.UNIT_NAME_POINT:
retVal = 1.0/72.0;
break;
}
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
return retVal;
}
module.exports.unitToInchFactor = unitToInchFactor;
/**
* Wait for a file to appear. Only works in UXP contexts with direct file access
*
* @function waitForFile
* @memberof crdtuxp
*
* @param {string} filePath - file that needs to appear
* @param {number=} interval - how often to check for file (milliseconds)
* @param {number=} timeout - how long to wait for file (milliseconds)
* @returns {Promise<boolean|undefined>} whether file appeared or not
*/
function waitForFile(
filePath,
interval = DEFAULT_WAIT_FILE_INTERVAL_MILLISECONDS,
timeout = DEFAULT_WAIT_FILE_TIMEOUT_MILLISECONDS) {
// coderstate: promisor
let retVal = RESOLVED_PROMISE_UNDEFINED;
do {
try {
var uxpContext = getUXPContext();
if (! uxpContext.hasDirectFileAccess) {
crdtuxp.logError(arguments, "need direct file access");
break;
}
let endTime = (new Date()).getTime() + timeout;
function checkFile() {
// coderstate: promisor
function checkFileExecutor(resolveFtn, rejectFtn) {
// coderstate: executor
const now = Date.now();
if (endTime < now) {
crdtuxp.logNote(arguments, "already timed out");
resolveFtn(undefined);
} else {
try {
// https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/fs/
const stats = uxpContext.fs.lstatSync(filePath);
if (stats) {
crdtuxp.logTrace(arguments, "file appeared");
resolveFtn(true);
} else {
crdtuxp.logNote(arguments, "no stats returned");
delayFunction(interval, checkFile).
then(
resolveFtn,
rejectFtn
);
}
}
catch (err) {
if (err != FILE_NOT_EXIST_ERROR) {
crdtuxp.logNote(arguments, "throws " + err);
}
else {
crdtuxp.logTrace(arguments, "file not present yet");
}
delayFunction(interval, checkFile).
then(
resolveFtn,
rejectFtn
);
}
}
};
return new Promise(checkFileExecutor);
};
retVal = checkFile();
}
catch (err) {
crdtuxp.logError(arguments, "throws " + err);
}
}
while (false);
return retVal;
}
module.exports.waitForFile = waitForFile;