forked from sheetjs/docs.sheetjs.com
247 lines
6.7 KiB
Markdown
247 lines
6.7 KiB
Markdown
|
---
|
||
|
title: AppleScript and OSA
|
||
|
pagination_prev: demos/cloud/index
|
||
|
pagination_next: demos/bigdata/index
|
||
|
---
|
||
|
|
||
|
Open Scripting Architecture (OSA), a built-in feature in macOS introduced in
|
||
|
1993, enables users to communicate with applications with a standardized
|
||
|
language and grammar. macOS releases starting from Yosemite (OSX 10.10) include
|
||
|
native support for scripting with JavaScript.
|
||
|
|
||
|
The [Standalone scripts](/docs/getting-started/installation/standalone) can be
|
||
|
parsed and evaluated from the JS engine. Once evaluated, the `XLSX` variable is
|
||
|
available as a global. A JS stub can expose methods from AppleScript scripts.
|
||
|
|
||
|
:::note
|
||
|
|
||
|
This demo was last tested on 2022 April 18 in macOS Monterey.
|
||
|
|
||
|
:::
|
||
|
|
||
|
## Integration details
|
||
|
|
||
|
import Tabs from '@theme/Tabs';
|
||
|
import TabItem from '@theme/TabItem';
|
||
|
|
||
|
<Tabs groupId="osa">
|
||
|
<TabItem value="js" label="JavaScript">
|
||
|
|
||
|
The following snippet reads a file into a binary string:
|
||
|
|
||
|
```js
|
||
|
ObjC.import("Foundation");
|
||
|
function get_bstr(path) {
|
||
|
/* create NSString from the file contents using a binary encoding */
|
||
|
var str = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSISOLatin1StringEncoding, null);
|
||
|
/* return the value as a JS object */
|
||
|
return ObjC.unwrap(str);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
_Loading the Library_
|
||
|
|
||
|
Assuming the standalone library is in the same directory as the source file,
|
||
|
the script can be evaluated with `eval`:
|
||
|
|
||
|
```js
|
||
|
var src = get_bstr("./xlsx.full.min.js");
|
||
|
eval(src);
|
||
|
```
|
||
|
|
||
|
_Parsing Files_
|
||
|
|
||
|
The same method can be used to read binary strings and parse with `type: "binary"`:
|
||
|
|
||
|
```js
|
||
|
var file = get_bstr("./pres.numbers");
|
||
|
var wb = XLSX.read(file);
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="as" label="AppleScript">
|
||
|
|
||
|
The core idea is to push the processing logic to a stub JS file.
|
||
|
|
||
|
_JS Stub_
|
||
|
|
||
|
The JS stub will be evaluated in the JavaScript context. The same technique from
|
||
|
the JavaScript section works in the stub:
|
||
|
|
||
|
```js
|
||
|
ObjC.import("Foundation");
|
||
|
|
||
|
function get_bstr(path) {
|
||
|
var str = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSISOLatin1StringEncoding, null);
|
||
|
return ObjC.unwrap(str);
|
||
|
}
|
||
|
|
||
|
/* this will be called when AppleScript initializes the JS engine */
|
||
|
eval(get_bstr("./xlsx.full.min.js"));
|
||
|
```
|
||
|
|
||
|
It is more efficient to offload as much work as possible into the stub. For
|
||
|
example, this function parses a workbook file from the filesystem and generates
|
||
|
a CSV without passing intermediate values back to AppleScript:
|
||
|
|
||
|
```js
|
||
|
/* this method will be exposed as `wb_to_csv` */
|
||
|
function wb_to_csv(path) {
|
||
|
/* read file */
|
||
|
var filedata = get_bstr(path);
|
||
|
var wb = XLSX.read(filedata, { type: "binary" });
|
||
|
return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
_Loading the Stub_
|
||
|
|
||
|
Assuming the stub is saved to `xlsx.stub.js`, the following handler creates a
|
||
|
context and evaluates the standalone library:
|
||
|
|
||
|
```applescript
|
||
|
on getContext()
|
||
|
-- get contents of xlsx.stub.js
|
||
|
set UnixPath to POSIX path of ((path to me as text) & "::")
|
||
|
set libpath to POSIX path of (UnixPath & "xlsx.stub.js")
|
||
|
set {src, err} to current application's NSString's stringWithContentsOfFile:libpath encoding:(current application's NSISOLatin1StringEncoding) |error|:(reference)
|
||
|
if src is missing value then error (err's localizedDescription()) as text
|
||
|
|
||
|
-- create scripting context and evaluate the stub
|
||
|
set lang to current application's OSALanguage's languageForName:"JavaScript"
|
||
|
set osa to current application's OSAScript's alloc()'s initWithSource:src language:lang
|
||
|
return osa
|
||
|
end getContext
|
||
|
```
|
||
|
|
||
|
_Evaluating JS Code_
|
||
|
|
||
|
When calling a function, the result is an array whose first item is the value of
|
||
|
the evaluated code. A small helper function extracts the raw result:
|
||
|
|
||
|
```applescript
|
||
|
on extractResult(res)
|
||
|
return item 1 of ((current application's NSArray's arrayWithObject:res) as list)
|
||
|
end extractResult
|
||
|
```
|
||
|
|
||
|
With everything defined, `executeHandlerWithName` will run functions defined in
|
||
|
the stub. For example:
|
||
|
|
||
|
```applescript
|
||
|
set osa to getContext()
|
||
|
set {res, err} to osa's executeHandlerWithName:"wb_to_csv" arguments:{"pres.numbers"} |error|:(reference)
|
||
|
extractResult(res)
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
## Complete Demo
|
||
|
|
||
|
This example will read from a specified filename and print the first worksheet
|
||
|
data in CSV format.
|
||
|
|
||
|
0) Download the standalone script and test file:
|
||
|
|
||
|
```bash
|
||
|
curl -LO https://sheetjs.com/pres.numbers
|
||
|
curl -LO https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js
|
||
|
```
|
||
|
|
||
|
<Tabs groupId="osa">
|
||
|
<TabItem value="js" label="JavaScript">
|
||
|
|
||
|
1) Save the following script to `sheetosa.js`:
|
||
|
|
||
|
```js title="sheetosa.js"
|
||
|
#!/usr/bin/env osascript -l JavaScript
|
||
|
|
||
|
ObjC.import("Foundation");
|
||
|
function get_bstr(path) {
|
||
|
var str = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSISOLatin1StringEncoding, null);
|
||
|
return ObjC.unwrap(str);
|
||
|
}
|
||
|
eval(get_bstr("./xlsx.full.min.js"));
|
||
|
|
||
|
function run(argv) {
|
||
|
var filedata = get_bstr(argv[0]);
|
||
|
var wb = XLSX.read(filedata, { type: "binary" });
|
||
|
console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]));
|
||
|
}
|
||
|
```
|
||
|
|
||
|
2) Make the script executable:
|
||
|
|
||
|
```bash
|
||
|
chmod +x sheetosa.js
|
||
|
```
|
||
|
|
||
|
3) Run the script, passing the path to the test file as an argument:
|
||
|
|
||
|
```bash
|
||
|
./sheetosa.js pres.numbers
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="as" label="AppleScript">
|
||
|
|
||
|
1) Save the following script to `xlsx.stub.js`:
|
||
|
|
||
|
```js title="xlsx.stub.js"
|
||
|
ObjC.import("Foundation");
|
||
|
function get_bstr(path) {
|
||
|
var str = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSISOLatin1StringEncoding, null);
|
||
|
return ObjC.unwrap(str);
|
||
|
}
|
||
|
eval(get_bstr("./xlsx.full.min.js"));
|
||
|
|
||
|
function wb_to_csv(path) {
|
||
|
var filedata = get_bstr(path);
|
||
|
var wb = XLSX.read(filedata, { type: "binary" });
|
||
|
return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
2) Save the following script to `sheetosa.scpt`:
|
||
|
|
||
|
```applescript title="sheetosa.scpt"
|
||
|
#!/usr/bin/env osascript
|
||
|
use AppleScript version "2.7"
|
||
|
use scripting additions
|
||
|
use framework "Foundation"
|
||
|
use framework "OSAKit"
|
||
|
|
||
|
set osa to getContext()
|
||
|
set {res, err} to osa's executeHandlerWithName:"wb_to_csv" arguments:{"pres.numbers"} |error|:(reference)
|
||
|
extractResult(res)
|
||
|
|
||
|
on getContext()
|
||
|
set UnixPath to POSIX path of ((path to me as text) & "::")
|
||
|
set libpath to POSIX path of (UnixPath & "xlsx.shim.js")
|
||
|
set {src, err} to current application's NSString's stringWithContentsOfFile:libpath encoding:(current application's NSISOLatin1StringEncoding) |error|:(reference)
|
||
|
|
||
|
set lang to current application's OSALanguage's languageForName:"JavaScript"
|
||
|
set osa to current application's OSAScript's alloc()'s initWithSource:src language:lang
|
||
|
return osa
|
||
|
end getContext
|
||
|
|
||
|
on extractResult(res)
|
||
|
return item 1 of ((current application's NSArray's arrayWithObject:res) as list)
|
||
|
end extractResult
|
||
|
```
|
||
|
|
||
|
3) Make the script executable:
|
||
|
|
||
|
```bash
|
||
|
chmod +x sheetosa.scpt
|
||
|
```
|
||
|
|
||
|
3) Run the script (it is hardcoded to read `pres.numbers`):
|
||
|
|
||
|
```bash
|
||
|
./sheetosa.scpt
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|