Exo: JavaScript unleashed!

Post your working scripts, libraries and tools for AHK v1.1 and older
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

29 Dec 2014, 09:09

I just noticed there are some functions, like _ControlGetPos, which return an object instead of calling inject(). As a result, the value is not returned to JavaScript.


I was playing around, and came up with this for built-in vars:

Code: Select all

w := wb.document.parentWindow
w.eval("
(
function _DefVars_(names, GetVar) {
	names.split(' ').forEach(function(name) {
		Object.defineProperty(window, name, {
			get: function() {
				return GetVar(name)
			}});
	});
}
)")
w._DefVars_("x_AhkPath x_AhkVersion", Func("GetVar"))
GetVar(name) {
	name := "A" SubStr(name, 2)
	return (%name%)
}
Again, I used "x" to avoid conflict with Exo.js. All you need to do is provide a list of variable names. The same sort of thing can be done with most of the built-in functions.

Functions which return JavaScript objects can work like this:

Code: Select all

w := wb.document.parentWindow
global JavaScript := w.eval("({
(
Object: function() {
	var obj = {};
	for (var i = 0; i < arguments.length; i += 2)
		obj[arguments[i]] = arguments[i+1];
	return obj;
},
Array: function() {
	return Array.prototype.slice.call(arguments);
},
Export: function(name, func) {
	window[name] = func;
}
)})")
JavaScript.Export("GetTheMousePos", Func("x_MouseGetPos"))
x_MouseGetPos(Flag=""){
	MouseGetPos X, Y, Win, Control, %Flag%
	return JavaScript.Object("X",X, "Y",Y, "Win",Win, "Control",Control)
}
Here, the helper functions are only available to AutoHotkey, via the JavaScript variable. There's no need for an intermediary AutoHotkey function.
Last edited by lexikos on 29 Dec 2014, 09:13, edited 1 time in total.
Reason: Simplified JavaScript.Array()
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

29 Dec 2014, 12:13

And let me take it further: Loop is the only function that returns "Array of Objects".
With a little creative tinkering, that also works, based on your code:

Code: Select all

test(){
    loopResult := [{name:"file1",ext:"txt"},{name:"file2",ext:"txt"}]
	jsObjects := []
	for key, val in loopResult {
		params := []
		for k, v in val {
			params.Insert(k)
			params.Insert(v)
		}
		jsObject := JavaScript.Object.call("",params*) ; variadic call
		jsObjects.Insert(jsObject)
	}
    return JavaScript.Array.call("", jsObjects*)  ; variadic call
}

JavaScript.Export("test", Func("test"))
wb.document.parentWindow.eval("alert(JSON.stringify(test()))")
; output is: [{"ext":"txt","name":"file1"},{"ext":"txt","name":"file2"}]
I think we got it covered.
I should start implementing :).
bartlb
Posts: 6
Joined: 07 Apr 2014, 13:43

Re: Exo: JavaScript unleashed!

29 Dec 2014, 14:53

First and foremost, fantastic work here @Aurelain!

A quick question though, regarding the environment configurations that Exo has been tested on: Are there any known limitations concerning OS, IE, or AHK versions/builds?

I'm attempting to run the demo script (demo.js) but I keep getting the following error message:
Screenshot
My Configuration: UPDATE
It would appear that the JS Object method, Object.defineProperty(), was implemented in IE 8+ with a limited scope -- to DOM Objects only. It's not until IE 9 that the method was 'fully' implemented. Upgrading IE to a later version (IE 9, or greater) has resolved this issue. However, as a suggestion, I think it would be worth noting this in a minimal configuration settings section within your README file on github.
Last edited by bartlb on 29 Dec 2014, 15:33, edited 1 time in total.
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

29 Dec 2014, 15:21

Indeed, I will have to add a Minimum requirements section.
From what I can see, your problem is IE8.
It doesn't support getters/setters very well, and the AHK built-in variables have been implemented this way (e.g. A_Now, not A_Now()).

I believe the current Minimum requirements are: Win XP (?), AHK 1.1 (soon I'll require 1.1.17.00), IE9.
I will be more rigorous when I update the readme.
Thanks for the report ;).
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

29 Dec 2014, 15:30

It would be great if we could emulate some of the AHK directives manually.
For example, #SingleInstance could be emulated by setting it to Off in Exo.ahk and then manually killing other instances if JS says Force.

There are 2 issues here:

1. What syntax should we use in JS?
A function-like syntax (i.e. SingleInstance("Force")) is misleading, because directives are special.
I would like to use custom comments similar to JSHint.
Syntax could be simply: /* #SingleInstance Force */
Sidenote: This way, we could also implement the request by Menixator to clear up the global scope. i.e. /* #GlobalScope false */

2. Which directives can be implemented and how?
First about the "how". There may be some exotic ways (call AutoHotkey.exe, maybe use "eval()" by Fincs), but they're unpleasant. So, does anyone know manually (DllCall?) ways to achieve this?
The list of directives of interest to JS:
Spoiler
The most important ones seem to be possible: #Include, #NoTrayIcon, #Persistent, #SingleInstance.
Coco
Posts: 771
Joined: 29 Sep 2013, 20:37
Contact:

Re: Exo: JavaScript unleashed!

29 Dec 2014, 15:37

Aurelain wrote:call AutoHotkey.exe, maybe use "eval()" by Fincs
I believe eval() no longer works with the latest AHK version. And it wouldn't work since Directives aren't expressions.
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

29 Dec 2014, 17:28

Aurelain wrote:I believe the current Minimum requirements are: Win XP (?), AHK 1.1 (soon I'll require 1.1.17.00), IE9.
IE9 requires Vista SP2 or later. It cannot be installed on Windows XP.
A function-like syntax (i.e. SingleInstance("Force")) is misleading, because directives are special.
It's only misleading if it doesn't behave like a function. I think most of the directives which are applicable to Exo could be functions which simply change the settings at the point they are called. It can be more flexible this way. For instance, you could call them dynamically; something you can't do in AutoHotkey, which is going to be a problem for Exo.
And let me take it further: Loop is the only function that returns "Array of Objects".
Your example creates an AutoHotkey array and objects, and then converts them, but the Loop function has no need for that. It can work like this:

Code: Select all

output := JavaScript.Array()
...
output.push(JavaScript.Object(
(
    "Name", A_LoopRegName,
    "Type", A_LoopRegType,
    "Key", A_LoopRegKey,
    "SubKey", A_LoopRegSubKey,
    "TimeModified", A_LoopRegTimeModified
)))
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

30 Dec 2014, 06:06

bartlb wrote: It would appear that the JS Object method, Object.defineProperty(), was implemented in IE 8+ with a limited scope -- to DOM Objects only.
I have good news. "window" is a DOM object, and it should work in IE8.
Why did it not work in your case?
Because the page was running in compatibility mode (about:blank seems to do this in IE8). In the next release, I'm switching to an actual ".html" file (no more about:blank), and it works properly in IE8. ;)
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

30 Dec 2014, 23:11

On a related note, I found this to be a reliable way to get the latest document mode, without a file:

Code: Select all

Gui Add, ActiveX, vWB w500 h500, about:<!doctype html><meta http-equiv="X-UA-Compatible" content="IE=edge">
No need for Navigate or about:blank. None of the other without-a-file methods - mshtml:%html%, document.write() and IPersistStreamInit - give the proper result. Loading the HTML from a DLL/EXE or CHM file works - I tested res://ieframe.dll/rmbase.htm and ms-its:%A_AhkPath%\..\AutoHotkey.chm::/docs/AutoHotkey.htm.
running in compatibility mode (about:blank seems to do this in IE8)
It's not just IE8, and I think not IE itself. I've got the following results using the WebBrowser control:
  • IE6 on Windows 2000 shows compatMode == "BackCompat".
  • IE8 or later shows documentMode == 5.0.
  • Writing the HTML5 doctype with document.write() changes documentMode to 7.0.
  • Writing the X-UA-Compatible tag with document.write() changes documentMode to 8.0, on both IE8 and IE11.
  • Initializing the control with about: and X-UA-Compatible gives documentMode 11.0 on IE11.
Coco
Posts: 771
Joined: 29 Sep 2013, 20:37
Contact:

Re: Exo: JavaScript unleashed!

30 Dec 2014, 23:34

lexikos wrote:Initializing the control with about: and X-UA-Compatible gives documentMode 11.0 on IE11.
Nice. I've been searching for this solution for a while now (to avoid the registry modification and/or loading a file).
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: Exo: JavaScript unleashed!

31 Dec 2014, 01:57

Very nice
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

31 Dec 2014, 02:11

Nice find. Unfortunately, the new release will use an actual html file, for the reasons in the following excerpt:

Code: Select all

/*
The HTML code must be hosted by a local file (file://), not by "about:blank".
Reasons:
	● "window.onload()" will work in JS.
	● Allows relative paths in HTML src attributes (for <img/>, for example).
Therefore, we need to write a temporary ".html" file next to the main JS file.
We make it hidden and remove it once the page has loaded.
*/
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

31 Dec 2014, 04:31

Wouldn't <base> also allow relative paths?

If onload doesn't fire, you could always fire it yourself, if needed, but I don't see why it would be.
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

01 Jan 2015, 11:20

lexikos wrote:Wouldn't <base> also allow relative paths?
Lexikos is talking about the <base> tag.
Yes, I've tested it, it works. Thanks :).
lexikos wrote: If onload doesn't fire, you could always fire it yourself, if needed, but I don't see why it would be.
I couldn't find a way to manually trigger the load event in IE8.
But it doesn't matter, because I've found that window.onload does trigger if you don't use document.write and instead place all the HTML code inside the "about:" url.

Code: Select all

wb.Navigate("about:<script>window.onload=function(){alert('ok');}</script>") ; <-- works!
wb.Navigate("about:blank")
wb.document.write("<script>window.onload=function(){alert('ok');}</script>") ; <-- doesn't work
To sum up:
I will go with the about:<HTML> syntax and not use the hidden "*.html" file.
The only minor issues this method has is an ugly window.location and any possible aberrations caused by <base>.
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

01 Jan 2015, 18:01

This works with IE11 and IE9:

Code: Select all

Gui Add, ActiveX, vwb w500 h500, about:<!doctype html>`n<meta http-equiv="X-UA-Compatible" content="IE=edge">
Gui Show
while wb.ReadyState != 4
    Sleep 10
wb.document.open()  ; Without this, the next write() erases our first script.
wb.document.parentWindow.eval("
(
    Object.defineProperty(window, 'location', {value: {href: 'ahk://fubar'}});
)")
wb.document.write("<script>window.onload=function(){alert('ok @ '+window.location.href);}</script>")
wb.document.close()  ; This fires window.onload().
ExitApp
According to some reading I've done, it isn't possible to change window.location as it's a "non-configurable property". But in IE11, Object.getOwnPropertyDescriptor(window, 'location') returns undefined, and defineProperty works just fine. In Chrome 38, it indicates that location is indeed not configurable, but we can still redefine it with defineProperty.

Edit: Object.defineProperty() can't be used in this manner on IE8 or if document.documentMode <= 8. I've also found that on IE8, document.write() resets the document mode, so you need to include the doctype etc. in the HTML you write as well as in the URL.

Edit: Posted an overview on StackOverflow.
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

New release

02 Jan 2015, 09:29

Most of the issues discussed in this thread have been fixed/implemented in the latest release.
Changelog v0.95:
  • Major restructure of the JS-AHK communication. It is now much more direct (and presumably faster) since it uses the new features of AutoHotkey v1.1.17.01.
  • Major restructure of the HTML host. It is now a virtual page that works in IE8+. It should be free of race conditions and window.onload works fine.
  • Major code reshuffle. Removed all libraries except "API.ahk" from "/lib". Reduced a lot of code boilerplate.
  • Implemented all g-labels. They now have a similar behavior as in AHK: you only need to declare function GuiClose(){} and it will be auto-activated when the time comes.
  • GuiClose and OnExit can prevent ExitApp by returning false(or 0)
  • Functions that have no return value (therefore undefined) now return the empty string. This was done for the sake of code brevity and it should be harmless.
  • Renamed Parameters to A_Args
  • OnMessage now follows the documentation
  • Marked SetTimer as "Cannot use yet"
  • Added a JS specific function: Require()
  • Fixed a lot of bugs in API
  • Added Lexikos in the About section
Coco
Posts: 771
Joined: 29 Sep 2013, 20:37
Contact:

Re: Exo: JavaScript unleashed!

02 Jan 2015, 13:45

FileOpen() Test:
I opted to wrap the FileObject into a class since we would need to implement/overwrite RawRead/RawWrite to have it return JScript-compatible data for the VarOrAdrress parameter. RawRead/RawWrite is not yet implemented in the demo but other FileObject methods/properties should work.
(your editor must be able to capture stdout for this demo, or you can run it from the console and pipe the output)

Code: Select all

sc := ComObjCreate("ScriptControl")
sc.Language := "JScript"
sc.AddCode("
(
function hello() {
	stdout = FileOpen('*', 'w');
	stdout.WriteLine('Hello World');
	stdout.Close();
}
)")

sc.AddObject("FileOpen", Func("_FileOpen"))
sc.Eval("hello()")
sc := ""
return

_FileOpen(fspec, flags, encoding:="CP0")
{
	return new FileObject(fspec, flags, encoding)
}

class FileObject
{
	__New(fspec, flags, encoding:="CP0")
	{
		this.__Ptr := FileOpen(fspec, flags, encoding)
	}

	__Get(key, args*)
	{
		if (key ~= "i)^__Handle|AtEOF|Encoding|Length|Pos(ition)?$")
			return (this.__Ptr)[key, args*]
	}

	__Set(key, value, args*)
	{
		if (key ~= "i)^Encoding|Length|Pos(ition)?$")
			return (this.__Ptr)[key] := value
	}

	__Call(method, args*)
	{
		if (method ~= "i)^(Read|Write)(Line|U?(Char|Short|Int)|Double|Float|Int64)?|Seek|Tell|Close$")
			return (this.__Ptr)[method](args*)
	}

	RawRead(ByRef VarOrAddress, bytes)
	{
		; need to wrap VarOrAddress to JScript-compatible object
	}

	RawWrite(ByRef VarOrAddress, bytes)
	{
		; need to wrap VarOrAddress to JScript-compatible object
	}
}
VarSetCapacity() proof of concept:

Code: Select all

sc := ComObjCreate("ScriptControl")
sc.Language := "JScript"
sc.AddCode("
(
function test() {
	var shell = new ActiveXObject('WScript.Shell');

	var RECT = VarSetCapacity(16, 0);
	shell.Popup('RECT size: ' + RECT.Size);

	// put data
	for (var i = 0; i < 4; i++) {
		RECT.PutInt(i + 1);
	}

	// read data
	var offset;
	for (var i = 0; i < 4; i++) {
		offset = i * 4
		shell.Popup('Offset ' + offset + ': ' + RECT.GetInt(offset));
	}

	// write and read string
	var buf = VarSetCapacity(256); // ensure size
	buf.WriteUTF8('Hello World');
	buf.Seek(0); // reset pointer
	shell.Popup(buf.ReadUTF8());
}
)")

sc.AddObject("VarSetCapacity", Func("_VarSetCapacity"))
sc.Eval("test()")
sc := ""
return

_VarSetCapacity(size, fillbyte:=0)
{
	return new AhkBuffer(size, fillbyte)
}

/* Buffer class
 *
 * NumPut|NumGet routines
 * Methods:
 *     PutNumType(num [, offset])
 *     GetNumType([, offset])
 *         - NumType can be any of the AHK num types. If offset is
 *           omitted, the internal pointer is used.
 * 
 * StrPut|StrGet routines
 * Methods:
 *     Read([length, enc])
 *     Write(str [, length, enc])
 *     ReadEnc([length])
 *     WriteEnc(str [, length])
 *         - where 'Enc' can be 'UTF(8|16)' OR 'CPn'. 'CP0' if omitted
 *
 * Others:
 *     Seek(distance [, origin := 0]) - Similar to FileObject.Seek()
 *     Size - returns the size of the buffer
 */
class AhkBuffer ;// temporary name, rename to something cooler
{
	__New(size, fillbyte:=0)
	{
		ObjSetCapacity(this, "_", Abs(size))
		addr := ObjGetAddress(this, "_")
		DllCall("RtlFillMemory", "Ptr", addr, "UPtr", size, "UChar", fillbyte)
		this.Pos := this.__Ptr := addr
	}

	__Call(method, args*)
	{
		static ObjPush := Func(A_AhkVersion < "2" ? "ObjInsert" : "ObjPush")

		if (method = "Put" || method = "Get")
			method .= "UPtr"
		if (method = "Read" || method = "Write")
			method .= "CP0"

		if (method ~= "i)^((Put|Get)|(Read|Write))[A-W0-9]+$")
		{
			n := 0
			if RW := InStr("RW", SubStr(method, 1, 1))
				n := InStr(method, "r")
			%ObjPush%(args, SubStr(method, 4 + n))
			return this[RW ? "_Str" : "_Num"](SubStr(method, 1, 3 + n), args*)
		}
	}

	_Num(action, num:="UPtr", at:="UPtr", type:="UPtr")
	{
		if (action = "Get") ; adjust the arguments
			type := at, at := num
		if (at ~= "i)^U?(Char|Short|Int|Ptr)|Double|Float|Int64$")
			type := at, at := this.Pos
		
		ptr := this.__Ptr
		if !(at >= ptr && at < ptr + this.Size)
			at := ptr + at

		if (action = "Put")
			return this.Pos := NumPut(num, at + 0, type)

		static sizeof := { "Char": 1, "Short": 2, "Int": 4, "Float": 4
		                 , "Double": 8, "Int64": 8, "Ptr": A_PtrSize }
		this.Pos += sizeof[LTrim(type, "Uu")]
		
		return NumGet(at + 0, type)
	}

	_Str(action, args*)
	{
		enc := "CP0"
		for i, arg in args
		{
			if (arg ~= "i)^UTF-?(8|16)|CP\d+$")
			{
				if InStr(enc := arg, "UTF")
					args[i] := enc := "UTF-" . LTrim(SubStr(enc, 4), "-")
				break
			}
		}
		static ObjRemoveAt := Func(A_AhkVersion < "2" ? "ObjRemove" : "ObjRemoveAt")
		addr := this.Pos
		str := (read := (action = "read")) ? StrGet(addr, args*) : %ObjRemoveAt%(args, 1)

		BytesPerChar := (enc = "UTF-16" || enc = "CP1600") ? 2 : 1
		this.Pos += (StrPut(str, enc) * BytesPerChar)

		return read ? str : StrPut(str, addr, args*)
	}

	Size {
		get {
			return ObjGetCapacity(this, "_")
		}
	}

	Seek(distance, origin:=0)
	{
		if (distance < 0 && origin != 2)
			origin := 2
		start := origin == 0 ? this.__Ptr
		      :  origin == 1 ? this.Pos
		      :  origin == 2 ? this.__Ptr + this.Size
		      :  0
		return start ? this.Pos := start + distance : 0
	}
}
Last edited by Coco on 02 Jan 2015, 15:38, edited 1 time in total.
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: Exo: JavaScript unleashed!

02 Jan 2015, 14:41

Very nice!!!
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
Aurelain
Posts: 34
Joined: 02 Sep 2014, 15:37

Re: Exo: JavaScript unleashed!

02 Jan 2015, 18:14

Thanks, Coco. I've implemented the FileOpen function using your proposal.
Relevant code: I'm not very familiar with the modern features of AHK, but I think it works properly.

P.S. I couldn't get RawWrite to accept a variadic call (unknown reasons, maybe I was doing something wrong), so I left it as a distinct function, not handled by __Call.
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exo: JavaScript unleashed!

02 Jan 2015, 19:07

Aurelain wrote:P.S. I couldn't get RawWrite to accept a variadic call
You can't pass a variable by reference via a variadic call or to a variadic function. .RawWrite(VarOrAddress, bytes) is like NumPut(1, VarOrAddress); it will use the variable itself as its target. In this case, that would be useless because VarOrAddress is a parameter/local variable. If you were calling RawWrite() from AutoHotkey and wanted to retain the usage, you would need to declare it ByRef. However, as Coco hinted, that concept is not JScript-compatible; you can't pass variables by reference in JScript.

The simplest solution is to allow only an address, and pass it like .RawWrite(Address+0, bytes). However, you would need some way to get an address in JScript - it would be better to use something like an ArrayBuffer or DataView (but these require IE10+).
Coco wrote:VarSetCapacity() proof of concept
It looks useful, and like it would be used for some of the same things as VarSetCapacity, but I don't see the benefit in naming it the same when the usage is different. Also, the usage looks similar to a DataView object.

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 132 guests