id = $id; $this->url = $url; $this->orig_url = $url; $this->orig_html = $orig_html; if (!$this->id) { $this->id = md5($this->url ?: uniqid('debloat_script', true)); } // Has to be processed early as it might have to be searched for things like defer. $this->process_content(); } /** * Factory method: Create an instance provided an HTML tag. * * @param string $tag * @return boolean|self */ public static function from_tag(string $tag) { $attrs = Util\parse_attrs($tag); // Only process scripts of type javascript or missing type (assumed JS by browser). if (isset($attrs['type']) && strpos($attrs['type'], 'javascript') === false) { return false; } $script = new self($attrs['id'] ?? '', $attrs['src'] ?? '', $tag); // Note: We keep 'id' just in case if there was an original id. $script->attrs = \array_diff_key($attrs, array_flip(['src', 'defer', 'async'])); if (isset($attrs['defer'])) { $script->defer = true; } if (isset($attrs['async'])) { $script->async = true; } return $script; } /** * Render the HTML. * * @return string */ public function render() { // Add src if available. if ($this->url) { $this->attrs += [ 'src' => $this->get_content_url(), 'id' => $this->id, ]; } $this->process_delay(); // Setting local to avoid mutating. $content = $this->content; if (!$this->url && $content && is_callable($this->minifier)) { $content = call_user_func($this->minifier, $content); } // Add defer for external scripts. For inline scripts, use data uri in src. if ($this->defer) { $this->attrs['defer'] = true; if (!$this->url) { $this->attrs['src'] = 'data:text/javascript;base64,' . base64_encode($content); $content = ''; } } if ($this->async) { $this->attrs['async'] = true; } $render_atts = implode(' ', $this->render_attrs($this->attrs)); $new_tag = sprintf( '%2$s', $render_atts ? " $render_atts" : '', !$this->url ? $content : '' ); return $new_tag; } /** * Set the inner content, if any. * * @return void */ public function process_content() { if (!$this->url) { $this->content = preg_replace('#]*>(.+?)\s*$#is', '\\1', $this->orig_html); } } /** * Set a callable minifier function. * * @param callable $minifier * @return void */ public function set_minifier($minifier) { $this->minifier = $minifier; } /** * Process and add delayed script attributes if needed. * * @return void */ public function process_delay() { if (!$this->delay) { return; } // Defer / async shouldn't be enabled anymore. Delay load logic implies deferred anyways. $this->defer = false; $this->async = false; // Add delay. $this->attrs['data-debloat-delay'] = 1; // Normal script with a URL. if (!empty($this->attrs['src'])) { $this->attrs['data-src'] = $this->attrs['src']; } // Inline script with content. elseif ($this->content) { if (!empty($this->attrs['type'])) { $this->attrs['data-type'] = $this->attrs['type']; } $this->attrs['type'] = 'text/debloat-script'; } unset($this->attrs['src']); } }