Skip to content

Commit

Permalink
Translate feature
Browse files Browse the repository at this point in the history
* Bump version 
* Update deps
* Add JSDocs to all functions
* Improve validation
* Add "Translate" feature
  • Loading branch information
TheJaredWilcurt authored Nov 25, 2023
1 parent 5aa65eb commit ee23ce4
Show file tree
Hide file tree
Showing 6 changed files with 1,186 additions and 534 deletions.
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ if (process.platform === 'win32') {
## Documentation


### getWindowsShortcutProperties.sync
### getWindowsShortcutProperties.sync API

* First argument: `filePath`
* **TYPE:** *String or Array of Strings*
Expand All @@ -105,7 +105,7 @@ if (process.platform === 'win32') {
* **DESCRIPTION:** This is an **optional** function that is called with a message and error object (if something fails, or you pass in bad inputs). Defaults to using `console.error` if not passed in.


### Output
### getWindowsShortcutProperties.sync Output

Returns `undefined` if all files errored, or an Array of Objects for each successful file:

Expand All @@ -131,6 +131,51 @@ See [Microsoft's Shortcut Documentation](https://docs.microsoft.com/en-us/troubl
If you pass in an array of files, and some succeed, you will get an array of the success and console errors for the other files (unless you pass in a `customLogger` function, in which case it gets called when errors occur).


### getWindowsShortcutProperties.translate API

* First argument: `shortcutProperties`
* **TYPE:** *Array of Objects*
* **DESCRIPTION:** Each object in the array is the properties for one shortcut, we convert this over to something more human readable.
* Second argument: `customLogger`
* **TYPE:** *function*
* **DESCRIPTION:** This is an **optional** function that is called with a message and error object (if something fails, or you pass in bad inputs). Defaults to using `console.error` if not passed in.


### getWindowsShortcutProperties.translate Output

Takes in the ouput of `getWindowsShortcutProperties.sync`, and then translates it into the Input for `create-desktop-shortcuts` (a different Node.js library).

```js
const microsoftNamingConventions = [
{
FullName: 'C:\\Users\\Owner\\Desktop\\DaVinci Resolve.lnk',
Arguments: '--foo',
Description: 'Video Editor',
Hotkey: 'Ctrl+F10',
IconLocation: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\ResolveIcon.exe,0',
RelativePath: '',
TargetPath: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\Resolve.exe',
WindowStyle: '1',
WorkingDirectory: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\'
}
];
const output = getWindowsShortcutProperties.translate(microsoftNamingConventions); // produces the below output
const output = [
{
filePath: 'C:\\Users\\Owner\\Desktop\\DaVinci Resolve.lnk',
arguments: '--foo',
comment: 'Video Editor',
hotkey: 'Ctrl+F10',
icon: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\ResolveIcon.exe,0',
relativePath: '',
targetPath: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\Resolve.exe',
windowMode: 'normal',
workingDirectory: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\'
}
];
```


* * *


Expand Down
278 changes: 216 additions & 62 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,40 @@ const path = require('path');

const parseRawData = require('./src/parse-raw-data.js');

/**
* OPTIONAL: console.error is called by default.
*
* Your own custom logging function called with helpful warning/error
* messages from the internal validators.
*
* @typedef {Function} CUSTOMLOGGER
* @callback {Function} CUSTOMLOGGER
* @param {string} message The human readable warning/error message
* @param {object} [error] Sometimes an error or options object is passed
* @return {void}
*/

/**
* @typedef {object} SHORTCUTPROPERITES
* @property {string} FullName 'C:\\Users\\Owner\\Desktop\\DaVinci Resolve.lnk'
* @property {string} Arguments '--foo=bar'
* @property {string} Description 'Video Editor'
* @property {string} Hotkey 'CTRL+SHIFT+F10'
* @property {string} IconLocation 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\ResolveIcon.exe,0'
* @property {string} RelativePath ''
* @property {string} TargetPath 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\Resolve.exe'
* @property {string} WindowStyle '1'
* @property {string} WorkingDirectory 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\'
*/

/**
* A generic function for errors/warnings. It will either call the passed in customLoger,
* or use console.error to notify the user of this library of errors or validation warnings.
*
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @param {string} message A human readable message describing an error/warning
* @param {any} error A programmatic error message or object, may be undefined
*/
function throwError (customLogger, message, error) {
if (typeof(customLogger) === 'function') {
customLogger(message, error);
Expand All @@ -23,41 +57,93 @@ function throwError (customLogger, message, error) {
}
}

function generateCommands (filePaths, customLogger) {
const commands = [];

for (let filePath of filePaths) {
const normalizedFile = normalizeFile(filePath, customLogger);
if (normalizedFile) {
// Escape (') and (’) in the file path for PowerShell syntax
const safeFilePath = normalizedFile
.replace(/'/g, "''")
.replace(//g, "’’");

const command = [
'(New-Object -COM WScript.Shell).CreateShortcut(\'',
safeFilePath,
'\');'
].join('');
commands.push(command);
}
/**
* Generic type validation function. Ensures value passed in is an array
* that contains at least one string, and only strings.
*
* @param {any} arr A value to be validated as an array of strings
* @return {boolean} true = valid
*/
function isArrayOfStrings (arr) {
let isValidArray = false;
if (Array.isArray(arr) && arr.length) {
isValidArray = arr.every(function (item) {
return typeof(item) === 'string';
});
}
return isValidArray;
}

return commands;
/**
* Generic type validation function. Ensures value passed in is an array
* that contains at least one object, and only objects.
*
* @param {any} arr A value to be validated as an array of objects
* @return {boolean} true = valid
*/
function isArrayOfObjects (arr) {
let isValidArray = false;
if (Array.isArray(arr) && arr.length) {
isValidArray = arr.every(function (item) {
return !Array.isArray(item) && typeof(item) === 'object';
});
}
return isValidArray;
}

function inputsAreValid (filePath, customLogger) {
let valid = true;
/**
* If a customLogger is passed in, ensures it is a valid function.
*
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @return {boolean} True if valid, false if invalid
*/
function customLoggerIsValid (customLogger) {
if (customLogger && typeof(customLogger) !== 'function') {
throwError(customLogger, 'The customLogger must be a function or undefined');
return false;
}
return true;
}

/**
* Validates that shortcutProperties and customLogger are the correct expected types.
*
* @param {SHORTCUTPROPERITES[]} shortcutProperties Array of objects, each representing a successful or failed shortcut property
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @return {boolean} True if valid, false if invalid
*/
function translateInputsAreValid (shortcutProperties, customLogger) {
let valid = true;
valid = customLoggerIsValid(customLogger);
if (!isArrayOfObjects(shortcutProperties)) {
throwError(customLogger, 'The shortcutProperties must be an array of objects');
valid = false;
}
return valid;
}

/**
* Validates that the filePath and customLogger are the correct expected types.
* Validates that this Windows specific library is actually being ran on Windows.
*
* @param {(string|string[])} filePath String or array of strings for the filepaths to shortcut files
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @return {boolean} True if valid, false if invalid
*/
function syncInputsAreValid (filePath, customLogger) {
let valid = true;
valid = customLoggerIsValid(customLogger);
if (process.platform !== 'win32') {
throwError(customLogger, 'Platform is not Windows');
valid = false;
}

if (
!filePath ||
(
Array.isArray(filePath) &&
!isArrayOfStrings(filePath)
) ||
(
!Array.isArray(filePath) &&
typeof(filePath) !== 'string'
Expand All @@ -69,6 +155,14 @@ function inputsAreValid (filePath, customLogger) {
return valid;
}

/**
* Normalizes a file path and ensures it exists and ends with .lnk or .url.
* Warns and returns false if filePath does not meet these requirements.
*
* @param {string} filePath Path to a .lnk or .url Windows shortcut file
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @return {string} The normalized full path to a Windows shortcut that is known to exist
*/
function normalizeFile (filePath, customLogger) {
const normalizedFile = path.normalize(path.resolve(filePath));
if (
Expand All @@ -87,53 +181,113 @@ function normalizeFile (filePath, customLogger) {
}

/**
* @callback customLoggerCallback
* @param {string} message
* @param {Error} error
*
* @typedef {{
* FullName: string,
* Arguments: string,
* Description: string,
* Hotkey: string,
* IconLocation: string,
* RelativePath: string,
* TargetPath: string,
* WindowStyle: string,
* WorkingDirectory: string
* }[]} shortcutProperties
* Creates strings of PowerShell commands for each filePath to get the file properties.
* Stores strings in a returned Array.
*
* @param {string[]} filePaths Array of strings for the filepaths to shortcut files
* @param {CUSTOMLOGGER} customLogger Optional function to handle logging human readable errors/warnings
* @return {string[]} Array of strings of PowerShell commands to get shortcut properties
*/
function generateCommands (filePaths, customLogger) {
const commands = [];

/**
* @param {string} filePath
* @param {customLoggerCallback} customLogger
* @returns {shortcutProperties}
*/
function getWindowsShortcutProperties (filePath, customLogger) {
if (!inputsAreValid(filePath, customLogger)) {
return;
}
if (typeof(filePath) === 'string') {
filePath = [filePath];
}
for (let filePath of filePaths) {
const normalizedFile = normalizeFile(filePath, customLogger);
if (normalizedFile) {
// Escape (') and (’) in the file path for PowerShell syntax
const safeFilePath = normalizedFile
.replace(/'/g, '\'\'')
.replace(//g, '’’');

const commands = generateCommands(filePath, customLogger).join('');
if (!commands || !commands.length) {
return;
}
const command = 'powershell.exe -command "' + commands + '"';
try {
const rawData = exec(command);
const parsed = parseRawData(rawData);
return parsed;
} catch (err) {
if (err) {
throwError(customLogger, 'Failed to run powershell command to get shortcut properties', err);
return;
const command = [
'(New-Object -COM WScript.Shell).CreateShortcut(\'',
safeFilePath,
'\');'
].join('');
commands.push(command);
}
}

return commands;
}

module.exports = {
sync: getWindowsShortcutProperties
/**
* Retrieves the details of OS based Windows shortcuts.
*
* @example
* const output = getWindowsShortcutProperties.sync([
* '../Sublime Text.lnk',
* 'C:\\Users\\Public\\Desktop\\Firefox.lnk'
* ]);
*
* @param {(string|string[])} filePath String or array of strings for the filepaths to shortcut files
* @param {CUSTOMLOGGER} customLogger Optional function to handle logging human readable errors/warnings
* @return {SHORTCUTPROPERITES[]} Array of objects or undefined, each representing a successful or failed shortcut property
*/
sync: function (filePath, customLogger) {
if (!syncInputsAreValid(filePath, customLogger)) {
return;
}
if (typeof(filePath) === 'string') {
filePath = [filePath];
}

const commands = generateCommands(filePath, customLogger).join('');
if (!commands || !commands.length) {
return;
}
const command = 'powershell.exe -command "' + commands + '"';
try {
const rawData = exec(command);
const parsed = parseRawData(rawData);
return parsed;
} catch (err) {
if (err) {
throwError(customLogger, 'Failed to run powershell command to get shortcut properties', err);
return;
}
}
},
/**
* Translates the official Microsoft shortcut property names to something more human readable and familiar to JavaScript developers.
*
* @param {SHORTCUTPROPERITES[]} shortcutProperties Array of objects, each representing a successful or failed shortcut property
* @param {CUSTOMLOGGER} customLogger User provided function to handle logging human readable errors/warnings
* @return {boolean} True if valid, false if invalid
*/
translate: function (shortcutProperties, customLogger) {
if (!translateInputsAreValid(shortcutProperties, customLogger)) {
return;
}
const windowModes = {
1: 'normal',
3: 'maximized',
7: 'minimized'
};
const translatedProperties = shortcutProperties.map(function (shortcut) {
const translatedShortcut = {
// 'C:\\Users\\Owner\\Desktop\\DaVinci Resolve.lnk',
filePath: shortcut.FullName || '',
// '--foo=bar',
arguments: shortcut.Arguments || '',
// 'Video Editor',
comment: shortcut.Description || '',
// 'CTRL+SHIFT+F10',
hotkey: shortcut.Hotkey || '',
// 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\ResolveIcon.exe,0',
icon: shortcut.IconLocation || '',
// '',
relativePath: shortcut.RelativePath || '',
// 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\Resolve.exe',
targetPath: shortcut.TargetPath || '',
// '1',
windowMode: windowModes[shortcut.WindowStyle] || 'normal',
// 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\',
workingDirectory: shortcut.WorkingDirectory || ''
};
return translatedShortcut;
});
return translatedProperties;
}
};
Loading

0 comments on commit ee23ce4

Please sign in to comment.