XML parsing syntax for OS X Yosemite JavaScript for Automation? - javascript
Has anyone deduced syntax which successfully loads XML from file/string and gives access to the data in OS X Yosemite (10.10) Javascript for Automation ?
Documentation and code samples are still fairly thin (as of Nov 2014), and my inductive skills are running dry on three separate approaches to reading an XML (OPML) file at the moment:
Most promising: $.NSXMLDocument
Getting hold of the string data in various ways goes well,
function readTextFromFile(strPath) {
return $.NSString.stringWithContentsOfFile(strPath);
}
function readDataFromFile(strPath) {
return $.NSData.alloc.initWithContentsOfFile(strPath);
}
function filePath(strPath) { return Path(strPath); }
But no permutations on this theme have borne fruit:
var strPath='/Users/houthakker/Desktop/notes-2014-11-04.opml',
dataXML = readData(strPath),
strXML = readTextFile(strPath),
oXMLDoc1, oXMLDoc2;
oXMLDoc1 = $.NSXMLDocument.alloc.initWithXMLString(strXML,0,null);
oXMLDoc2 = $.NSXMLDocument.alloc.initWithData(dataXML,0,null);
(the 'function undefined' error messages suggest that those two init functions may not be exposed, though initWithRootElement() does seem to be)
Most progress: $.NSXMLParser
var oParser = $.NSXMLParser.alloc.initWithData(dataXML);
return oParser.parse; //true
But event-driven parsing seems to require some further complexities which remain opaque to me, and which may not match my simple needs (reading and converting modestly sized local OPML files).
Most familiar: Application("System Events")
In Applescript this can be done with System Events code:
set fOPML to (POSIX file "/Users/houthakker/Desktop/notes-2014-11-04.opml" as alias) as string
tell application "System Events"
tell XML file fOPML
-- now access XML Elements by name or index
end tell
but I haven't found a successful javascript idiom for initializing the XMLFile object with any permutation of a unix Path(), string, or colon-delimited mac path string.
Any thoughts or more successful experience here ?
This turns out to work for the (very slow executing) Application("System Events") route:
var app = Application("System Events"),
strPath = '~/Desktop/summarise.opml';
var oFile = app.xmlFiles[strPath],
oRoot = oFile.xmlElements[0],
oHead = oRoot.xmlElements.head,
oBody = oRoot.xmlElements.body,
lstOutlineXML = oBody.xmlElements.whose({
name: 'outline'
});
And the function for initialising an NSXMLDocument from an XML string is, according to the JSExport convention (in which the letter following each ":" is capitalized, and then the ":"s are removed) .initWithXMLStringOptionsError()
Thus, to choose a local OPML file and parse it to a simple JSON outline:
function run() {
var app = Application.currentApplication();
app.includeStandardAdditions = true;
function readTextFromFile(strPath) {
return $.NSString.stringWithContentsOfFile(strPath);
}
var strPath = (
app.chooseFile({withPrompt: "Choose OPML file:"})
).toString(), // Path → String
strXML = strPath ? readTextFromFile(strPath) : '';
if (!strXML) return;
var oXMLDoc1 = $.NSXMLDocument.alloc.initWithXMLStringOptionsError(strXML, 0, null),
oRoot = oXMLDoc1.rootElement,
oBody = ObjC.unwrap(oRoot.children)[1],
lstOutline = ObjC.unwrap(oBody.children),
lstParse, strJSON;
function parseOPML(lst) {
var lstParse=[], oNode, dctNode, lstChiln;
for (var i = 0, lng=lst.length; i<lng; i++) {
oNode = lst[i];
dctNode = {};
dctNode.txt = ObjC.unwrap(oNode.attributeForName('text').stringValue);
lstChiln = ObjC.unwrap(oNode.children);
if (lstChiln && lstChiln.length)
dctNode.chiln = parseOPML(lstChiln);
lstParse.push(dctNode);
}
return lstParse;
}
lstParse = parseOPML(lstOutline);
strJSON = JSON.stringify(lstParse,null, 2);
app.setTheClipboardTo(strJSON);
return strJSON;
}
Related
Permissions error when running another JS AppleScript from another JSAppleScript
I am trying to separate out my .applescript files into different ones to tidy things up. I have a JS AppleScript file called Test.applescript that tries to run the JS AppleScript file Group Tracks Dependency.applescript and what I want to do is pass in a parameter into the dependency script and get a return value out of it. (It creates an array of arrays of iTunes tracks). Test.applescript (function() { var app = Application('iTunes'); app.includeStandardAdditions = true; app.doShellScript('Group Tracks Dependency.applescript'); return "Done"; })(); // For quick logging function log(obj) { this.console.log(obj); } Group Tracks Dependency.applescript (function(selection) { return getGroupsOfTracks(selection); function getGroupsOfTracks(originalTracksArray) { if (originalTracksArray == null || originalTracksArray.length == 0) return null; var tracks = originalTracksArray.slice(); var groups = []; while (true) { var group = []; group.push(tracks[0]); tracks = tracks.slice(1); while (true) { if (!tracks[0]) break; if (tracks[0].album() != group[0].album()) break; if (tracks[0].artist() != group[0].artist()) break; if (tracks[0].discNumber() != group[0].discNumber()) break; group.push(tracks[0]); tracks = tracks.slice(1); } groups.push(group); if (!tracks[0]) break; } return groups; } })(); When I try to run the Test script I get this error (line 5 is the app.doShellScript line): Error on line 5: Error: A privilege violation occurred. Is there any way to get around this? I should also note that I want other people to be able to download these scripts and run them on their own iTunes libraries in the future (currently it's not user-friendly though). If there's no way to get around this then would importing another JS AppleScript file work?
I think you may be fighting a battle that you can’t win using .doShellScript. The Apple way is to use a Script Library as defined on https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-11.html#//apple_ref/doc/uid/TP40014508-CH110-SW1 Unfortunately a script library has constraints where you can only pass simple variables. Another way is to use require, which can be defined with code like https://github.com/dtinth/JXA-Cookbook/wiki/Importing-Scripts
I'm not sure what you are trying to accomplish, but this works for me using Script Editor 2.8.1 (183.1) on OSX 10.11.4: Create a main JXA Script file Create a JXA Script Library file BOTH of these MUST be saved as compiled script files (.scpt) It is INCORRECT that "Unfortunately a script library has constraints where you can only pass simple variables." You can call any of the functions in the Script Library file from any JXA script. In your MAIN script file, which I will call "Get iTunes Group Selection.scpt": var app = Application('iTunes'); app.includeStandardAdditions = true; var myLib = Library("My JXA Lib") var selectionArr = app.selection() // ### Change as needed ### var groupArr = myLib.getGroupsOfTracks(selectionArr) groupArr ~~~~~~~~~~~~~~~~~~~~~ And then in a separate script file, saved as: ~/Library/Script Libraries/My JXA Lib.scpt function getGroupsOfTracks(originalTracksArray) { if (originalTracksArray == null || originalTracksArray.length == 0) return null; var tracks = originalTracksArray.slice(); var groups = []; while (true) { var group = []; group.push(tracks[0]); tracks = tracks.slice(1); while (true) { if (!tracks[0]) break; if (tracks[0].album() != group[0].album()) break; if (tracks[0].artist() != group[0].artist()) break; if (tracks[0].discNumber() != group[0].discNumber()) break; group.push(tracks[0]); tracks = tracks.slice(1); } groups.push(group); if (!tracks[0]) break; } return groups; }
Well, it's been a few years... I ran into errors with JXA and doShellScript when I tried to run with Application("Finder"). These errors went away when I instead ran the script from Application.currentApplication(). So for my script, I used const finder = Application("Finder") for Finder specific stuff, then const app = Application.currentApplication() for running the script. For example: //test1.scpt function run() { const app = Application.currentApplication() app.includeStandardAdditions = true app.doShellScript("osascript ~/Desktop/test2.scpt") } //test2.scpt function run() { const app = Application.currentApplication() app.includeStandardAdditions = true app.displayDialog("foo") app.doShellScript("osascript -e 'display dialog \"bar\"'") } As expected, running test1.scpt gives me two dialogs: foo and `bar.
flatbuffers is not defined
I'm writing a nodeJs application that uses google flat buffer. I installed flatc on my macbook pro and compiled the following schema: namespace MyAlcoholist; table Drink { drink_type_name: string; drink_company_name: string; drink_brand_name: string; drink_flavor_type_name : string; liquid_color_type_name : string; liquid_color_is_transparent : bool; alcohol_vol : float; calories_for_100g : uint; global_image_id: ulong; drink_flavor_id: ulong; } table Drinks { drinks:[Drink]; } root_type Drinks; the schema file is called drink.fbs and it generated a javascript file called drink_generated.js I include this file in my nodejs application and add data to it using the following code.. this is my flatBufferUtil.js utility file. var flatbuffers = require('../js/flatbuffers').flatbuffers; var builder = new flatbuffers.Builder(); var drinks = require('../fbs/drinks_generated').MyAlcoholist; // Generated by `flatc`. function drinkArrayToBuffer(drinkArray) { var drinksVectArray = []; drinkArray.forEach(function (element, index, array) { var drinkObj = element; var drinkBrandName = builder.createString(drinkObj.drink_brand_name); var drinkCompanyName = builder.createString(drinkObj.drink_company_name); var drinkflavorTypeName = builder.createString(drinkObj.drink_flavor_type_name); var drinkTypeName = builder.createString(drinkObj.drink_type_name); var liquidColorTypeName = builder.createString(drinkObj.liquid_color_type_name); drinks.Drink.startDrink(builder); drinks.Drink.addAlcoholVol(builder, drinkObj.alcohol_vol); drinks.Drink.addCaloriesFor100g(builder,drinkObj.calories_for_100g); drinks.Drink.addDrinkBrandName(builder,drinkBrandName); drinks.Drink.addDrinkCompanyName(builder,drinkCompanyName); drinks.Drink.addDrinkFlavorId(builder,drinkObj.drink_flavor_id); drinks.Drink.addDrinkFlavorTypeName(builder, drinkflavorTypeName); drinks.Drink.addDrinkTypeName(builder,drinkTypeName); drinks.Drink.addGlobalImageId(builder,drinkObj.global_image_id); drinks.Drink.addLiquidColorIsTransparent(builder,drinkObj.is_transparent); drinks.Drink.addLiquidColorTypeName(builder,liquidColorTypeName); var drink = drinks.Drink.endDrink(builder); drinksVectArray.push(drink); }) var drinksVect = drinks.createDrinksVector(builder,drinksVectArray); builder.finish(drinksVect); var buf = builder.dataBuffer(); return buf; } module.exports.drinkArrayToBuffer=drinkArrayToBuffer; now when I execute this function it fails with the error flatbuffers is not defined. I debugged my code and I saw that it files on the following line of code: drinks.Drink.addDrinkFlavorId(builder,drinkObj.drink_flavor_id); if i get inside addDrinkFlavorId function i see this code in drinks_generted.js: MyAlcoholist.Drink.addDrinkFlavorId = function(builder, drinkFlavorId) { builder.addFieldInt64(9, drinkFlavorId, flatbuffers.Long.ZERO); }; as you can see it uses flatbuffers.Long.ZERO but flatbuffers is not defined in that file at all. the compilation did not provide any errors so what do I miss?
It seems to me like it is a bug... The generated file appears to be meant to exist autonomously from the flatbuffers require. However for the custom flatbuffers.Long class, the default of flatbuffers.Long.ZERO bleeds into the generated file. While this isn't a solution per-say, one workaround is to manually add the flatbuffers require to the generated file; it's ugly, but it might be better than being blocked until a more appropriate answer (or fix) comes around. // In `drinks_generated.js` var flatbuffers = require('../js/flatbuffers').flatbuffers; Note: The drinks.Drink.addDrinkFlavorId() and drinks.Drink.addGlobalImageId() functions expect flatbuffers.Long values to be passed into them, because they were specified as ulong in the schema (fbs file). So you will need to ensure that you are not trying to pass in a simple number type. For example: var my_long = flatbuffers.Long(100, 0); // low = 100, high = 0 drinks.Drink.addDrinkFlavorId(builder, my_long); As a result, another possible workaround is to change the datatype of those fields in the schema to avoid using ulong until it becomes more clear what is going on here. P.S. I am pretty sure drinks.createDrinksVector on line 30 of that snippet should be drinks.Drinks.createDrinksVector.
Error when decoding base64 with buffer. First argument needs to be a number
I am working on running different encoding statements based on the URL. My code: var parsedUrl = url.parse(req.url, true); var queryAsObject = parsedUrl.query; var myString = queryAsObject["string"]; var myFunction = queryAsObject["function"]; if (myFunction == "encodeb64") { var bufEncode = new Buffer(JSON.stringify(myString)); var myEncode = bufEncode.toString('base64'); console.log(myEncode); } else { console.log("Error1"); }; if (myFunction == "decodeb64") { // var bufDecode = new Buffer(myEncode, 'base64'); // var myDecode = bufDecode.toString('utf8'); var myDecode = new Buffer(myEncode, 'base64').toString('utf8'); console.log(myDecode); } else { console.log("Error2"); }; URL used: http://127.0.0.1:8020/?string=text&function=decodeb64 The issue is that I am having is with the last if statement. If its looking for decodeb64 and the first statement is looking for encodeb64 it crashes when function=decodeb64 is in the URL. If both if statements are looking for either encodeb64 or decodeb64, it runs perfectly. It also works if function=encodeb64 is in the URL. The error message I get is: buffer.js:188 throw new TypeError('First argument needs to be a number, ' + ^ It points to: var myDecode = new Buffer(myEncode, 'base64').toString('utf8'); The given number on the error is pointed to the n in new. I have located the problem to be inside the decode if statement by moving and reversing the order on the code. As you can see in my code notes, I have tried two different methods of decoding with no success.
The reason that it crashes I believe is that when function=decode64, the variable myEncode is not declared and initialized, as the if(myFunction=="encode64") block is not run. So when the code tried to new Buffer(myEncode...) it would fail as myEncode is undefined. I think you meant to code : var myDecode = new Buffer(myString, ...) instead
converting array buffers to string
I'm getting some weird results when converting an array buffer to a string then displaying the output in a div. I'm getting some GPS data from the USB port in a chrome packaged app. It converts the array buffer received from the port into a string and outputs. The functions are: var onReceiveCallback = function(info) { if (info.connectionId == connectionId && info.data) { $(".output").append(ab2str(info.data)); } }; /* Interprets an ArrayBuffer as UTF-8 encoded string data. */ var ab2str = function(buf) { var bufView = new Uint8Array(buf); var encodedString = String.fromCharCode.apply(null, bufView); return decodeURIComponent(escape(encodedString)); }; I have a start and stop button to obviously start and stop the reading of data from the gps device. When I start it the first time it works and outputs as expected, something like: $GPGGA,214948.209,,,,,0,0,,,M,,M,,*41 $GPGSA,A,1,,,,,,,,,,,,,,,*1E $GPGSV,1,1,01,07,,,33*7F $GPRMC,214948.209,V,,,,,0.00,0.00,270814,,,N*4C $GPGGA,214949.209,,,,,0,0,,,M,,M,,*40 $GPGSA,A,1,,,,,,,,,,,,,,,*1E $GPGSV,1,1,01,07,,,34*78 $GPRMC,214949.209,V,,,,,0.00,0.00,270814,,,N*4D but then when I stop it, and restart it, although I clear the output div, the output data seems to be mixing in with the previous result. Like: $$GPGPGGGGAA,,221155115544..202099,,,,,,,,,0,0,0,0,,,,,,MM,,,,MM,,,,**4455 $$GGPPGGSSAA,,AA,,11,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,**11EE $$GGPGPGSSVV,,11,,11,,0022,,0077,,,,,,3344,1,177,,,,,,3311**77FF $$GGPPRRMMCC,,212155115544..220099,,VV,,,,,,,,,,00..0000,,00..0000,,227700881144,,,,,,NN*4*488 $$GPGGPGGGAA,,221155115555..220099,,,,,,,,,,00,,00,,,,,,MM,,,,MM,,,,**4444 $$GGPPGGSSAA,,AA,,11,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,**11EE $G$GPPGGSSVV,,11,,11,,0022,,0077,,,,,,331,1,1177,,,,,,2255**77FF $$GGPPRRMMCC,2,21155115555..220099,,VV,,,,,,,,,,00..0000,,00..0000,,227700881144,,,,,,N*N*4499 Its like a buffer or variable isnt being emptied, or something else crazy that I cant figure out. Any pointers appreciated. edit: this is the 'start' function which clears the output div and reconnects: // when the start button is clicked $( "#start" ).click(function() { if ( deviceId == 0 ) { console.log("Please select a device"); return; } else { $(".output").empty(); serial.connect(deviceId, {bitrate: 9600}, onConnect); } });
I have found this technique unreliable in my own code, although I don't remember if the problem was similar to one you report: var ab2str = function(buf) { // not reliable var bufView = new Uint8Array(buf); var encodedString = String.fromCharCode.apply(null, bufView); return decodeURIComponent(escape(encodedString)); }; So, I have done it this way, with code taken from one of the Google Chrome App examples (tcpserver): function ab2str(buf, callback) { var bb = new Blob([new Uint8Array(buf)]); var f = new FileReader(); f.onload = function(e) { callback(e.target.result); }; f.readAsText(bb); } Note that this version isn't an exact replacement, since it's asynchronous. Now, starting with Chrome Version 38 (now in beta), you can do it this way: function ab2str(buf) { var dataView = new DataView(buf); var decoder = new TextDecoder('utf-8'); return decoder.decode(dataView); } As I always run the beta and am preparing examples for a forthcoming book, I am now doing it the newest way. Give that a try and see if your problem goes away. If not, my suggestion to examine info.data is still a good one, I think. UPDATE: I've just checked out this reverse function, which you may also find handy at some point: function str2ab(buf) { var encoder = new TextEncoder('utf-8'); return encoder.encode(buf).buffer; }
this._get is not a function - javascript oop and prototypes
I'm using something similar to NodeJS called bondi, it's build on the Firefox js engine.. Basically i'm getting this error and I believe it's due to the way i'm referencing "this" in the .Get function below. Basically there is a tool called SFtpClient. It has the method of "Get", to list the contents of a folder, but I want to change the prototype for this with a drop in include file. I need to change it so that it a/ retries several times when it fails, and b/ it has a recursive folder listing function. So I used the prototype to change it - moved .Get to ._Get. Can anyone see why I would be getting the error: Jan 23 04:51:34 beta bondi: === this._Get is not a function --- Prio(6) Result(0x0) File(/home/nwo/approot/include/sftpclientenh when I run the code below? Thanks SFtpClient.prototype._Get = SFtpClient.prototype.Get; SFtpClient.prototype.Get = function(Folder, Retries){ //defaults if(!Retries) Retries = 5; if(!Folder) Folder = "~/"; //vars var FileListing = []; var connect = function(){ //TODO JRF 19.01.2012 : re-enable this when bondi is fixed // this.HomeDirectory.replace(/\/?$/, "/"); FileListing = this._Get(Folder); return true; } var i = 1; do{ var res = false; try { res = connect(); }catch(e){ Debug.LogInfo(e.message); } i++; Server.Sleep(i*2000); } while(res==false && i < Retries); return FileListing; }
Try res = connect.call(this) instead of res = connect().