Need to JSON stringify an object in ExtendScript - javascript

I am working on processing meta data information of my Indesign document links, using ExtentdScript.
I want to convert the object to string using JSON.stringify but when I use it, I am getting error saying:
can't execute script in target engine.
If I remove linkObjStr = JSON.stringify(linksInfObj); from below code, then everything works fine.
What is the equivalent to JSON.stringify in ExtendScript, or is there any other possibilities to display linksInfObj with its proper contents instead [object object]?
for (var i = 0, len = doc.links.length; i < len; i++) {
var linkFilepath = File(doc.links[i].filePath).fsName;
var linkFileName = doc.links[i].name;
var xmpFile = new XMPFile(linkFilepath, XMPConst.FILE_INDESIGN, XMPConst.OPEN_FOR_READ);
var allXMP = xmpFile.getXMP();
// Retrieve values from external links XMP.
var documentID = allXMP.getProperty(XMPConst.NS_XMP_MM, 'DocumentID', XMPConst.STRING);
var instanceID = allXMP.getProperty(XMPConst.NS_XMP_MM, 'InstanceID', XMPConst.STRING);
linksInfObj[linkFileName] = {'docId': documentID, 'insId': instanceID};
linkObjStr = JSON.stringify(linksInfObj);
alert('Object' + linksInfObj, true); // I am getting [Object Object] here
alert('String' + linkObjStr, true);
}

ExtendScript does not include a JSON object with the associated methods for parsing, namely JSON.parse() and JSON.stringify(). Nor does it provide any other builtin feature for parsing JSON.
Solution:
Consider utilizing a polyfill to provide JSON functionality such as JSON-js created by Douglas Crockford.
What you'll need to do:
Download the JavaScript file named json2.js from the Github repo and save it in the same location/folder as your .jsx file.
Note You can just copy and paste the raw version of json2.js from the same Github repo to create the json2.js file manually if you prefer.
Then at the top of your current .jsx file you'll need to #include the json2.js file by adding the following line of code:
#include "json2.js";
This is analogous to how you might utilize the import statement to include a module in modern day JavaScript (ES6).
A pathname to the location of the json2.js can be provided if you decide to save the file in a different location/folder than your .jsx file.
By including json2.js in your .jsx file you'll now have working JSON methods; JSON.parse() and JSON.stringify().
Example:
The following ExtendScript (.jsx) is a working example that generates JSON to indicate all the links associated with the current InDesign document (.indd).
example.jsx
#include "json2.js";
$.level=0;
var doc = app.activeDocument;
/**
* Loads the AdobeXMPScript library.
* #returns {Boolean} True if the library loaded successfully, otherwise false.
*/
function loadXMPLibrary() {
if (!ExternalObject.AdobeXMPScript) {
try {
ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
} catch (e) {
alert('Failed loading AdobeXMPScript library\n' + e.message, 'Error', true);
return false;
}
}
return true;
}
/**
* Obtains the values f XMP properties for `DocumentID` and `instanceID` in
* each linked file associated with an InDesign document (.indd). A returns the
* information formatted as JSON,
* #param {Object} doc - A reference to the .indd to check.
* #returns {String} - The information formatted as JSON.
*/
function getLinksInfoAsJson(doc) {
var linksInfObj = {};
linksInfObj['indd-name'] = doc.name;
linksInfObj.location = doc.filePath.fsName;
linksInfObj.links = [];
for (var i = 0, len = doc.links.length; i < len; i++) {
var linkFilepath = File(doc.links[i].filePath).fsName;
var linkFileName = doc.links[i].name;
var xmpFile = new XMPFile(linkFilepath, XMPConst.FILE_INDESIGN, XMPConst.OPEN_FOR_READ);
var allXMP = xmpFile.getXMP();
// Retrieve values from external links XMP.
var documentID = allXMP.getProperty(XMPConst.NS_XMP_MM, 'DocumentID', XMPConst.STRING);
var instanceID = allXMP.getProperty(XMPConst.NS_XMP_MM, 'InstanceID', XMPConst.STRING);
// Ensure we produce valid JSON...
// - When `instanceID` or `documentID` values equal `undefined` change to `null`.
// - When `instanceID` or `documentID` exist ensure it's a String.
instanceID = instanceID ? String(instanceID) : null;
documentID = documentID ? String(documentID) : null;
linksInfObj.links.push({
'name': linkFileName,
'path': linkFilepath,
'docId': documentID,
'insId': instanceID
});
}
return JSON.stringify(linksInfObj, null, 2);
}
if (loadXMPLibrary()) {
var linksJson = getLinksInfoAsJson(doc);
$.writeln(linksJson);
}
Output:
Running the script above will log JSON formatted something like the following example to your console:
{
"indd-name": "foobar.indd",
"location": "/path/to/the/document",
"links":[
{
"name": "one.psd",
"path": "/path/to/the/document/links/one.psd",
"docId": "5E3AE91C0E2AD0A57A0318E078A125D6",
"insId": "xmp.iid:0480117407206811AFFD9EEDCD311C32"
},
{
"name": "two.jpg",
"path": "/path/to/the/document/links/two.jpg",
"docId": "EDC4CCF902ED087F654B6AB54C57A833",
"insId": "xmp.iid:FE7F117407206811A61394AAF02B0DD6"
},
{
"name": "three.png",
"path": "/path/to/the/document/links/three.png",
"docId": null,
"insId": null
}
]
}
Sidenote: Modelling your JSON:
You'll have noticed that the JSON output (above) is structured differently to how you were attempting to structure it in your given example. The main difference is that you were using link filenames as property/key names, such as the following example:
Example of a problematic JSON structure
{
"one.psd": {
"docId": "5E3AE91C0E2AD0A57A0318E078A125D6",
"insId": "xmp.iid:0480117407206811AFFD9EEDCD311C32"
},
"two.jpg": {
"docId": "EDC4CCF902ED087F654B6AB54C57A833",
"insId": "xmp.iid:FE7F117407206811A61394AAF02B0DD6"
}
...
}
Producing JSON like this example isn't ideal because if you were to have two links, both with the same name, you would only ever report one of them. You cannot have two properties/keys that have the same name within an Object.
Edit:
As a response to the OP's comment:
Hi RobC, other than using #include 'json2.js', is there any other way to include external js file in the JSX file?
There are a couple of alternative ways as follows:
You could utilize $.evalFile(). For instance replace #include "json2.js"; with the following two lines:
var json2 = File($.fileName).path + "/" + "json2.js";
$.evalFile(json2);
Note: This example assumes json2.js resides in the same folder as your .jsx
Alternatively, if you're wanting to avoid the existence of the additional json2.js file completely. You could add a IIFE (Immediately Invoked Function Expression) at the top of your .jsx file. Then copy and paste the content of the json2.js file into it. For instance:
(function () {
// <-- Paste the content of `json2.js` here.
})();
Note: If code size is a concern then consider minifying the content of json2.js before pasting it into the IIFE.

I apply JavaScript Minifier to JSON-js
then put the result to my script.

Related

In Karate DSL, calling a javascript file returns java.lang.RuntimeException

I have a javascript file I want to call. contents are below. When I tried calling the file, I keep getting a "no variable found with name: response" even though there is clearly a variable defined. The file executes fine within command-line using node so the javascript function is valid. Any thoughts? I attached the error message in a screenshot.
Javascript content in snippet below.
Karate script:
Scenario: Call JavaScript:
* def sample = read('classpath:reusable/gen-data.js')
* print someValue
function createTestData(sampleJson, fieldsToChange, numRecords) {
var testData = [];
for (var i = 0; i < numRecords; i++) {
var copy = JSON.parse(JSON.stringify(sampleJson));
fieldsToChange.forEach(function(fieldToChange) {
copy[fieldToChange] = copy[fieldToChange] + i;
});
testData.push(copy);
}
return {content: testData};
}
var testData = {
"country": "US",
"taskStatusCode" : "Closed",
"facilityCode" : "US_203532",
};
function getTestData() {
String testData = JSON.stringify(createTestData(testData, ["taskStatusCode", "facilityCode"], 1), null, 1);
console.log("all done getTestData()");
console.log("test data: \n" + testData);
return testData;
};
console.log("calling getTestData()");
getTestData();
I think this error is thrown when the JavaScript is not correct. For example in my case this JS file:
/* Set the custom authentication header */
function fn() {
var authToken = karate.get('authToken');
var out = {};
out['Auth-Token'] = authToken
return out;
}
This file will produce the "no variable found with name: response".
The reason is because "the right-hand-side (or contents of the *.js file if applicable) should begin with the function keyword." according to the karate docs (link).
Now by moving the comment and making the function keyword the first bit of text it works as expected:
function fn() {
/* Set the custom authentication header */
var authToken = karate.get('authToken');
var out = {};
out['Auth-Token'] = authToken
return out;
}
In the OP, the function keyword is the first thing in the file, but there is javascript outside the original function -- which I don't think is legal for karate syntax. In other words, everything has to be in the outer function.
My workaround was to use java instead of JavaScript.

Google Drive REST API v2 file properties array in Google Apps Script is undefined?

I am working to manage some Google Drive files with Google Apps Script. One piece of this project is reviewing properties on files, so I am using the Drive API rather than DriveApp. Additionally, Google Apps Script currently has access to the Drive REST API v2 instead of v3.
I've successfully set a property (id) and am able to pull the files with the property set.
console = Logger;
function Files (folderId) {
var optionalArgs,response
;
optionalArgs = {
q:'"'+folderId+'" in parents',
spaces:"drive"
}
do {
response = Drive.Files.list(optionalArgs);
response.items.forEach(function (file) {
console.log(file.properties);
var id = file.properties.find(function (property) {
return property.key === 'id';
});
this[id.value] = file;
},this);
} while (optionalArgs.pageToken = response.nextPageToken);
}
When running this function, I am able to see the file properties in the log
[{visibility=PUBLIC, kind=drive#property, etag="3GjDSTzy841RsmcBo4Ir-DLlp20/HGzJl78t8I2IehiAlaGXTkm2-C4", value=9e18766b-1cc9-4c1b-8003-b241f43db304, key=id}]
but get
TypeError :Cannot call method "find" of undefined.
I am unable to iterate through this resulting array. Using JSON.parse on it trys to convert it to an object, which is problematic for files with multiple properties. Using JSON.parse(JSON.stringify()) on it results in
SyntaxError: Unexpected token: u
which I understand is resulting from the value being undefined. I could work with that, if my log wasn't telling me otherwise.
"nextPageToken(string) The page token for the next page of files. This will be absent if the end of the files list has been reached."
Quote from: https://developers.google.com/drive/v2/reference/files/list#examples
so this line assigns an undefined value to optionalArgs.pageToken
while (optionalArgs.pageToken = response.nextPageToken);
and does not terminate the loop. A better way is to assign the value of optionalArgs.pageToken inside the loop and break the loop if its value is undefined like this:
console = Logger;
function Files (folderId) {
var optionalArgs,response
;
optionalArgs = {
q:'"'+folderId+'" in parents',
spaces:"drive"
}
do {
response = Drive.Files.list(optionalArgs);
response.items.forEach(function (file) {
console.log(file.properties);
var id = file.properties.find(function (property) {
return property.key === 'id';
});
this[id.value] = file;
},this);
// Assign the value inside the loop
optionalArgs.pageToken = response.nextPageToken
} while (optionalArgs.pageToken != undefined) // check to see if it undefined here and break the loop.
}
You can use v3 in Google Apps Script by selecting the version in the Advance Google Service option:
Just note that you have to follow how to request in Google Drive v3 but you can still use v2 because all of the example is still in v2.
Regarding your error, using their code in the sample code for Listing folders.
function listRootFolders() {
var query = '"root" in parents and trashed = false and ' +
'mimeType = "application/vnd.google-apps.folder"';
var folders, pageToken;
do {
folders = Drive.Files.list({
q: query,
maxResults: 100,
pageToken: pageToken
});
if (folders.items && folders.items.length > 0) {
for (var i = 0; i < folders.items.length; i++) {
var folder = folders.items[i];
Logger.log('%s (ID: %s)', folder.title, folder.id);
}
} else {
Logger.log('No folders found.');
}
pageToken = folders.nextPageToken;
} while (pageToken);
}
You can also read more code implementation of the above documentation.
Hope this helps.

Bytes to image from Parse.com

I'm trying to convert a Parse.com object into an image with Javascript. The object is of the type Bytes, and I can't convert it in any way. Can someone give me an example of how to turn it into a functional URL? I try to access it but it just keeps crashing.
Edit:
getURL: function () {
var query = new Parse.Query("Token");
query.limit(20);
query.find().then(this.handleCallback.bind(this));
},
handleCallback: function (objects) {
for (var i = 0; i < objects.length; i++) {
this.tokenSearch[i] = {
imgURL: "data:image/png;base64," + objects[i].get("pic")
};
}
}
I have tried objects[i].get("pic").url(), objects[i].get("pic").toString('base64') and some other stuff, but it won't work!
If you want to store images and later have a URL you can load, you should switch to the File data type instead of pushing just a byte array. The documentation you linked recommends the following:
There are two ways to store binary data. The Bytes type allows you to associate NSData/bytes[] types directly on a PFObject. This is recommended only for small pieces of binary-encoded data. For actual files (images, documents, etc.), the File type can be used by instantiating a PFFile/ParseFile and setting it on a field.
You have stored the actual byte array into the pic column of your Parse class. Therefore, that's all you get. If you chose instead to store it as a File (PFFile in iOS/OSX, ParseFile in Android), you could do what you're trying to do. As it is, Parse is merely storing the bytes as it would store any other data, such as a Date or a String. There's no URL data associated with it.
Here's how you can create and upload a PFFile from an iOS device:
- (void)updateloadImage:(UIImage *)image {
PFFile *file = [PFFile fileWithData:UIImagePNGRepresentation(image)];
PFObject *newToken = [PFObject objectWithClassName:#"Token"];
newToken[#"pic"] = file;
[newToken saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
// Do real error handling here
// Now, you have :
// 1. The "pic" saved to Parse as a PFFile
// 2. The "token" saved to Parse as a PFObject
// 3. The "pic" stored as a property on the "token"
}
];
}
Once you have it stored that way on Parse, you would access it in the JavaScript SDK something like this:
function getURL() {
var query = new Parse.Query("Token");
query.limit(20);
query.find().then(function (objects) {
for (var i = 0; i < objects.length; i = i + 1) {
this.tokenSearch[i] = {imgURL: objects[i].get("pic").url()};
}
});
}

Trying to get JavaScript code completion to work in netbeans with jsdoc

In my app.js I have the following:
angular.module('app').controller('userList',
['appSettings'
,function (/*#type {app.appSettings}*/appSettings) {
appSettings.<<== it shows a list here but nothing from autocomplete.js
In my autocomplete.js I have the following (generated by JavaScript printing out my services and their members):
var app={};
app.appSettings={};
app.appSettings.userFailMessages={};
app.appSettings.userFailMessages.invalidJson
="Invalid request, user sent is not valid json.";
NetBeans refuses to code complete appSettings for me and doesn't seem to know it's defined in autocomplete.js. Maybe I'm getting my js doc wrong but tried a mix of combination of #var, #type and #param without success.
It code completes when I type app.appSettings. and gives me a list from autocomplete.js but I would like to know how I can tell NetBeans that the passed argument to the function is app.appSettings.
Maybe I should have autocomplete contain constructor functions instead of object literals as #type suggests a certain type and not an instance.
This is NetBeans 7.3.1
Was close to the answer, to have NetBeans use type you have to define the type. Then to indicate that the parameters passed to your angular module (or any function) are of a certain type I use the #param jsdoc
The angular module:
angular.module('app').controller('userList'
, ['$scope','appRules','appSettings'
,/**
* #param {app.appRules} appRules
* #param {app.appSettings} appSettings
* */
function ($scope,appRules,appSettings,$timeout) {
//<== here both appRules and appSettings give suggestions
// from autocomplete
autocomplete.js (not included in my html file but just there for code suggest)
/*#typedef {Object} app*/
var app={};
app.appRules={};
app.appRules.userIsInRole=function (user,role){};
app.appRules.general={};
app.appRules.general.isEmpty=function (val){};
app.appRules.general.isEmail=function (val){};
app.appSettings={};
app.appSettings.userFailMessages={};
app.appSettings.userFailMessages.invalidJson
="Invalid request, user sent is not valid json.";
app.appSettings.userFailMessages.noPrivilege
="You do not have the privileges needed to change this user.";
I ran the following code in the console on a page that contains my app to generate autocomplete.js:
var inj;
function allServices(mod, r) {
if (!r) {
r = {};
inj = angular.element(document.querySelector('[data-ng-app]')).injector().get;
}
angular.forEach(angular.module(mod).requires, function(m) {
allServices(m, r)
});
angular.forEach(angular.module(mod)._invokeQueue, function(a) {
try {
r[a[2][0]] = inj(a[2][0]);
} catch (e) {
}
});
return r;
};
var output=[];
function addOutput(names,prop){
if(names.length===1){
output.push('var ');
}
output.push(names.join('.'));
if(typeof prop === 'object'){
output.push('={};\n');
for(thing in prop){
//TODO: no arrays or route paths
if(/[0-9\/\\]/.test(thing)){
continue;
}
names.push(thing);
addOutput(names,prop[thing]);
}
}else{
output.push('=');
output.push(
(typeof prop === 'function')?
prop.toString():
JSON.stringify(prop)
);
output.push(';\n');
}
names.pop();
}
function createOutput(){
allMyServices = allServices('app');
addOutput(['app'],allMyServices);
console.log(output.join(''));
}
createOutput();

Unable to access element in JSON when keyname is unknown

I'm trying to retrieve the filename of gists on Github, from the data obtained from Github's API. I'm using javascript to access the data.
An example result can be found here: https://api.github.com/users/blaercom/gists. I've also copied a shortened version of the data below.
{
...
id: "4468273",
files: {
firstpost.md: {
type: "text/plain",
filename: "firstpost.md",
size: 16,
language: "Markdown"
}
}
...
}
I've been trying many things, but I can't seem to access the filename. This is because the 'files' object is not a list, but a key-value pair, where the key identifier matches the filename itself.
Things I've tried include
filename = files[0]['filename']
filename = files[0].filename
filename = files['filename']
Frankly, the only methods that work are variations of filename = files['firstpost.md']['filename'] but this isn't valid since I can not know the filename beforehand.
I'm sure it is possible to access the filename, but have been spending quite a while testing different methods. Help would be appreciated!
You can use for (var key in object) {}, here is an example using the data from your api call:
var filenames = [];
for (var filename in data[0].files) {
filenames.push(filename);
}
console.log(filenames); // ["firstpost.md"]
Here is a real example using your json response
var obj='your json data';
var fileNames=[];
for(var i in obj[0]['files'])
{
var fileName=obj[0]['files'][i]['filename'];
fileNames.push(fileName);
}
document.write(fileNames[0]); // firstpost.md
Example.
Update:
Another example using jsonp/script.
<script src="https://api.github.com/users/blaercom/gists?callback=myCallback"></script>
The callback function
function myCallback(o)
{
var obj=o.data;
var fileNames=[];
for(var i in obj[0]['files'])
{
var fileName=obj[0]['files'][i]['filename'];
fileNames.push(fileName);
}
document.write(fileNames[0]); // firstpost.md
}

Categories