@@ -289,7 +289,7 @@ pub const Page = struct {
289289 try Dump .writeHTML (self .doc .? , out );
290290 }
291291
292- pub fn fetchModuleSource (ctx : * anyopaque , specifier : []const u8 ) ! []const u8 {
292+ pub fn fetchModuleSource (ctx : * anyopaque , specifier : []const u8 ) ! ? []const u8 {
293293 const self : * Page = @ptrCast (@alignCast (ctx ));
294294
295295 log .debug ("fetch module: specifier: {s}" , .{specifier });
@@ -435,10 +435,18 @@ pub const Page = struct {
435435 // TODO fetch the script resources concurrently but execute them in the
436436 // declaration order for synchronous ones.
437437
438- // sasync stores scripts which can be run asynchronously.
438+ // async_scripts stores scripts which can be run asynchronously.
439439 // for now they are just run after the non-async one in order to
440440 // dispatch DOMContentLoaded the sooner as possible.
441- var sasync : std .ArrayListUnmanaged (Script ) = .{};
441+ var async_scripts : std .ArrayListUnmanaged (Script ) = .{};
442+
443+ // defer_scripts stores scripts which are meant to be deferred. For now
444+ // this doesn't have a huge impact, since normal scripts are parsed
445+ // after the document is loaded. But (a) we should fix that and (b)
446+ // this results in JavaScript being loaded in the same order as browsers
447+ // which can help debug issues (and might actually fix issues if websites
448+ // are expecting this execution order)
449+ var defer_scripts : std .ArrayListUnmanaged (Script ) = .{};
442450
443451 const root = parser .documentToNode (doc );
444452 const walker = Walker {};
@@ -455,11 +463,6 @@ pub const Page = struct {
455463
456464 // ignore non-js script.
457465 const script = try Script .init (e ) orelse continue ;
458- if (script .kind == .unknown ) continue ;
459-
460- // Ignore the defer attribute b/c we analyze all script
461- // after the document has been parsed.
462- // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
463466
464467 // TODO use fetchpriority
465468 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
@@ -470,22 +473,18 @@ pub const Page = struct {
470473 // > parsing and evaluated as soon as it is available.
471474 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
472475 if (script .is_async ) {
473- try sasync .append (arena , script );
476+ try async_scripts .append (arena , script );
477+ continue ;
478+ }
479+
480+ if (script .is_defer ) {
481+ try defer_scripts .append (arena , script );
474482 continue ;
475483 }
476484
477485 // TODO handle for attribute
478486 // TODO handle event attribute
479487
480- // TODO defer
481- // > This Boolean attribute is set to indicate to a browser
482- // > that the script is meant to be executed after the
483- // > document has been parsed, but before firing
484- // > DOMContentLoaded.
485- // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
486- // defer allow us to load a script w/o blocking the rest of
487- // evaluations.
488-
489488 // > Scripts without async, defer or type="module"
490489 // > attributes, as well as inline scripts without the
491490 // > type="module" attribute, are fetched and executed
@@ -497,7 +496,11 @@ pub const Page = struct {
497496 try parser .documentHTMLSetCurrentScript (html_doc , null );
498497 }
499498
500- // TODO wait for deferred scripts
499+ for (defer_scripts .items ) | s | {
500+ try parser .documentHTMLSetCurrentScript (html_doc , @ptrCast (s .element ));
501+ self .evalScript (& s ) catch | err | log .warn ("evaljs: {any}" , .{err });
502+ try parser .documentHTMLSetCurrentScript (html_doc , null );
503+ }
501504
502505 // dispatch DOMContentLoaded before the transition to "complete",
503506 // at the point where all subresources apart from async script elements
@@ -510,7 +513,7 @@ pub const Page = struct {
510513 _ = try parser .eventTargetDispatchEvent (parser .toEventTarget (parser .DocumentHTML , html_doc ), evt );
511514
512515 // eval async scripts.
513- for (sasync .items ) | s | {
516+ for (async_scripts .items ) | s | {
514517 try parser .documentHTMLSetCurrentScript (html_doc , @ptrCast (s .element ));
515518 self .evalScript (& s ) catch | err | log .warn ("evaljs: {any}" , .{err });
516519 try parser .documentHTMLSetCurrentScript (html_doc , null );
@@ -534,57 +537,42 @@ pub const Page = struct {
534537 // evalScript evaluates the src in priority.
535538 // if no src is present, we evaluate the text source.
536539 // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
537- fn evalScript (self : * Page , s : * const Script ) ! void {
538- self .current_script = s ;
539- defer self .current_script = null ;
540-
541- // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
542- const opt_src = try parser .elementGetAttribute (s .element , "src" );
543- if (opt_src ) | src | {
544- log .debug ("starting GET {s}" , .{src });
545-
546- self .fetchScript (s ) catch | err | {
547- switch (err ) {
548- FetchError .BadStatusCode = > return err ,
549-
550- // TODO If el's result is null, then fire an event named error at
551- // el, and return.
552- FetchError .NoBody = > return ,
540+ fn evalScript (self : * Page , script : * const Script ) ! void {
541+ const src = script .src orelse {
542+ // source is inline
543+ // TODO handle charset attribute
544+ if (try parser .nodeTextContent (parser .elementToNode (script .element ))) | text | {
545+ try script .eval (self , text );
546+ }
547+ return ;
548+ };
553549
554- FetchError .JsErr = > {}, // nothing to do here.
555- else = > return err ,
556- }
557- };
550+ self .current_script = script ;
551+ defer self .current_script = null ;
558552
559- // TODO If el's from an external file is true, then fire an event
560- // named load at el.
553+ log .debug ("starting GET {s}" , .{src });
561554
555+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
556+ const body = (try self .fetchData (src , null )) orelse {
557+ // TODO If el's result is null, then fire an event named error at
558+ // el, and return
562559 return ;
563- }
560+ };
564561
565- // TODO handle charset attribute
566- const opt_text = try parser .nodeTextContent (parser .elementToNode (s .element ));
567- if (opt_text ) | text | {
568- try s .eval (self , text );
569- return ;
570- }
562+ script .eval (self , body ) catch | err | switch (err ) {
563+ error .JsErr = > {}, // nothing to do here.
564+ else = > return err ,
565+ };
571566
572- // nothing has been loaded.
573- // TODO If el's result is null, then fire an event named error at
574- // el, and return.
567+ // TODO If el's from an external file is true, then fire an event
568+ // named load at el.
575569 }
576570
577- const FetchError = error {
578- BadStatusCode ,
579- NoBody ,
580- JsErr ,
581- };
582-
583571 // fetchData returns the data corresponding to the src target.
584572 // It resolves src using the page's uri.
585573 // If a base path is given, src is resolved according to the base first.
586574 // the caller owns the returned string
587- fn fetchData (self : * const Page , src : []const u8 , base : ? []const u8 ) ! []const u8 {
575+ fn fetchData (self : * const Page , src : []const u8 , base : ? []const u8 ) ! ? []const u8 {
588576 log .debug ("starting fetch {s}" , .{src });
589577
590578 const arena = self .arena ;
@@ -619,7 +607,7 @@ pub const Page = struct {
619607 log .info ("fetch {any}: {d}" , .{ url , header .status });
620608
621609 if (header .status != 200 ) {
622- return FetchError .BadStatusCode ;
610+ return error .BadStatusCode ;
623611 }
624612
625613 var arr : std .ArrayListUnmanaged (u8 ) = .{};
@@ -631,17 +619,12 @@ pub const Page = struct {
631619
632620 // check no body
633621 if (arr .items .len == 0 ) {
634- return FetchError . NoBody ;
622+ return null ;
635623 }
636624
637625 return arr .items ;
638626 }
639627
640- fn fetchScript (self : * Page , s : * const Script ) ! void {
641- const body = try self .fetchData (s .src , null );
642- try s .eval (self , body );
643- }
644-
645628 fn newHTTPRequest (self : * const Page , method : http.Request.Method , url : * const URL , opts : storage.cookie.LookupOpts ) ! http.Request {
646629 var request = try self .state .http_client .request (method , & url .uri );
647630 errdefer request .deinit ();
@@ -712,28 +695,42 @@ pub const Page = struct {
712695 }
713696
714697 const Script = struct {
715- element : * parser.Element ,
716698 kind : Kind ,
717699 is_async : bool ,
700+ is_defer : bool ,
701+ src : ? []const u8 ,
702+ element : * parser.Element ,
703+ // The javascript to load after we successfully load the script
704+ onload : ? []const u8 ,
718705
719- src : []const u8 ,
706+ // The javascript to load if we have an error executing the script
707+ // For now, we ignore this, since we still have a lot of errors that we
708+ // shouldn't
709+ //onerror: ?[]const u8,
720710
721711 const Kind = enum {
722- unknown ,
723- javascript ,
724712 module ,
713+ javascript ,
725714 };
726715
727716 fn init (e : * parser.Element ) ! ? Script {
728717 // ignore non-script tags
729718 const tag = try parser .elementHTMLGetTagType (@as (* parser .ElementHTML , @ptrCast (e )));
730- if (tag != .script ) return null ;
719+ if (tag != .script ) {
720+ return null ;
721+ }
722+
723+ const kind = parseKind (try parser .elementGetAttribute (e , "type" )) orelse {
724+ return null ;
725+ };
731726
732727 return .{
728+ .kind = kind ,
733729 .element = e ,
734- .kind = parseKind (try parser .elementGetAttribute (e , "type" )),
730+ .src = try parser .elementGetAttribute (e , "src" ),
731+ .onload = try parser .elementGetAttribute (e , "onload" ),
735732 .is_async = try parser .elementGetAttribute (e , "async" ) != null ,
736- .src = try parser .elementGetAttribute (e , "src " ) orelse "inline" ,
733+ .is_defer = try parser .elementGetAttribute (e , "defer " ) != null ,
737734 };
738735 }
739736
@@ -742,34 +739,47 @@ pub const Page = struct {
742739 // > type indicates that the script is a "classic script", containing
743740 // > JavaScript code.
744741 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
745- fn parseKind (stype : ? []const u8 ) Kind {
746- if (stype == null or stype .? .len == 0 ) return .javascript ;
747- if (std .mem .eql (u8 , stype .? , "application/javascript" )) return .javascript ;
748- if (std .mem .eql (u8 , stype .? , "text/javascript" )) return .javascript ;
749- if (std .mem .eql (u8 , stype .? , "module" )) return .module ;
742+ fn parseKind (script_type_ : ? []const u8 ) ? Kind {
743+ const script_type = script_type_ orelse return .javascript ;
744+ if (script_type .len == 0 ) {
745+ return .javascript ;
746+ }
747+
748+ if (std .mem .eql (u8 , script_type , "application/javascript" )) return .javascript ;
749+ if (std .mem .eql (u8 , script_type , "text/javascript" )) return .javascript ;
750+ if (std .mem .eql (u8 , script_type , "module" )) return .module ;
750751
751- return .unknown ;
752+ return null ;
752753 }
753754
754- fn eval (self : Script , page : * Page , body : []const u8 ) ! void {
755+ fn eval (self : * const Script , page : * Page , body : []const u8 ) ! void {
755756 var try_catch : Env.TryCatch = undefined ;
756757 try_catch .init (page .scope );
757758 defer try_catch .deinit ();
758759
760+ const src = self .src orelse "inline" ;
759761 const res = switch (self .kind ) {
760- .unknown = > return error .UnknownScript ,
761- .javascript = > page .scope .exec (body , self .src ),
762- .module = > page .scope .module (body , self .src ),
762+ .javascript = > page .scope .exec (body , src ),
763+ .module = > page .scope .module (body , src ),
763764 } catch {
764765 if (try try_catch .err (page .arena )) | msg | {
765- log .info ("eval script {s}: {s}" , .{ self . src , msg });
766+ log .info ("eval script {s}: {s}" , .{ src , msg });
766767 }
767- return FetchError .JsErr ;
768+ return error .JsErr ;
768769 };
769770
770771 if (builtin .mode == .Debug ) {
771772 const msg = try res .toString (page .arena );
772- log .debug ("eval script {s}: {s}" , .{ self .src , msg });
773+ log .debug ("eval script {s}: {s}" , .{ src , msg });
774+ }
775+
776+ if (self .onload ) | onload | {
777+ _ = page .scope .exec (onload , "script_on_load" ) catch {
778+ if (try try_catch .err (page .arena )) | msg | {
779+ log .info ("eval script onload {s}: {s}" , .{ src , msg });
780+ }
781+ return error .JsErr ;
782+ };
773783 }
774784 }
775785 };
0 commit comments