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> |