AngleSharp / AngleSharp.Js

:angel: Extends AngleSharp with a .NET-based JavaScript engine.
https://anglesharp.github.io
MIT License
103 stars 22 forks source link

Insert & Execute Javascript code in HTML page? #19

Closed LeMoussel closed 7 years ago

LeMoussel commented 8 years ago

Hi,

Is it possible to pass Javascript code back and execute JavaScript function/method from C#?

For example :

ScriptingService javascriptService = new ScriptingService();
IConfiguration config = Configuration.Default.WithDefaultLoader(setup => setup.IsResourceLoadingEnabled = true).With(javascriptService).WithCss();

string scriptJS  = @"<script> 
                         function getTitle() { return document.title; }
                    </script>";

IDocument ashDocument = await BrowsingContext.New(config).OpenAsync("http://html5test.com");

// How to insert scriptJS  & excute getTitle() function?

// Perhaps to execute getTitle() function ....
JsValue jsGetTitle = javascriptService.Engine.GetJint(ashDocument).GetValue("getTitle");
string documentTitle = jsGetTitle.Invoke().AsString();
Console.WriteLine("Document Title: {0}", documentTitle);
FlorianRappl commented 8 years ago

Potentially, the following file will cover your desired use-case: https://github.com/AngleSharp/AngleSharp.Scripting/blob/master/AngleSharp.Scripting.JavaScript.Tests/InteractionTests.cs

LeMoussel commented 8 years ago

Unfortunately no. All test method don't load external HTML page and insert Javascript code.

FlorianRappl commented 8 years ago

I mean, how would you do in pure JS? You would

Does none of these methods work for you?

LeMoussel commented 8 years ago

I would

  1. Load HTML page (eg http://httpbin.org/)
  2. Append a new script element with some JS functions to the loaded page (eg: scriptJS)
  3. Call / Execute JS functions (eg: getTitle()) append to the loaded page from C#
FlorianRappl commented 8 years ago

If the example looks like above (where you know the script's content) I would highly recommend using eval. As you already have access to Jint (responsible for handling the specific IDocument) that is also most elegant.

If you need to load the script from some external source then the way you describe is certainly the best one. Here AngleSharp does all the steps for you.

So yes, that sounds good to me.

One thing to be aware is the asynchronous execution. You probably won't have access to the result right away. This is something that will definitely improve in the near future, but right now its not optimal.

LeMoussel commented 8 years ago

I do this

var jsGetTitle = javascriptService.Engine.GetJint(ashDocument).Execute("document.Title;").GetCompletionValue().AsString();

But I got exception document is not defined.

FlorianRappl commented 8 years ago

Is window defined? Also you probably mean document.title as Title is the C# variant, but title is the JavaScript name.

LeMoussel commented 8 years ago

I'm confused. OK for document.title window is not define. With

javascriptService.Engine.GetJint(ashDocument).Execute("if (typeof window === 'undefined'){console.log('object: window is not available...');}");

I got object: window is not available...

FlorianRappl commented 8 years ago

The thing is that you execute that stuff directly from Jint, without providing the right execution layer. The way JavaScript engines are built there are is a stack of so called contexts. They all provide a base context, which contains elementary JavaScript objects, such as String, Number, Math, ... Then there would be a layer populated by the current instance of the Window interface.

AngleSharp provides this window for you. Therefore I can recommend two things:

This should be made more convenient I guess. Perhaps some more methods may be useful here.

LeMoussel commented 8 years ago

some progress :) No error with Evaluate like this

javascriptService.Engine.Evaluate("document.title;", new ScriptOptions() { Context = ashDocument.DefaultView, Document = ashDocument });

But Evaluate has no return type (void). Like you say this should be made more convenient. Maybe some Evaluate methods like Execute may be useful here. eg :

javascriptService.Engine.Evaluate("document.title;", new ScriptOptions() { Context = ashDocument.DefaultView, Document = ashDocument }).AsString();
LeMoussel commented 8 years ago

POC

In EngineInstance.cs, modiy RunScriptmethod like this

        public Jint.Native.JsValue RunScript(String source)
        {
            _engine.EnterExecutionContext(Lexicals, Variables, _window);
            Jint.Native.JsValue jsResult = _engine.Execute(source).GetCompletionValue();
            _engine.LeaveExecutionContext();
            return jsResult;
        }

In JavaScriptEngine.cs, Add Execute method

        public Jint.Native.JsValue Execute(String source, ScriptOptions options)
        {
            var objectContext = options.Context;
            var instance = default(EngineInstance);

            if (_contexts.TryGetValue(objectContext, out instance) == false)
                _contexts.Add(objectContext, instance = new EngineInstance(objectContext, _external));

            return instance.RunScript(source);
        }

Test

string jsGetTitle = javascriptService.Engine.Execute("document.title;", new ScriptOptions() { Context = ashDocument.DefaultView, Document = ashDocument }).AsString();
FlorianRappl commented 8 years ago

Nope, this won't happen.

Jint will always let you access the last return value on the stack. Therefore, there is no need to make such changes. Instead, an extension method is more useful.

The coupling to Jint should be as minimal as possible. Right now its already on the edge.