[Bps-public-commit] rt-extension-turbo branch master created. 03c2b8068dea8246eebeee3ba749f9ac22d580a6
BPS Git Server
git at git.bestpractical.com
Mon Aug 1 19:33:45 UTC 2022
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt-extension-turbo".
The branch, master has been created
at 03c2b8068dea8246eebeee3ba749f9ac22d580a6 (commit)
- Log -----------------------------------------------------------------
commit 03c2b8068dea8246eebeee3ba749f9ac22d580a6
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Mon Aug 1 15:22:09 2022 -0400
Add ToDo list
diff --git a/lib/RT/Extension/Turbo.pm b/lib/RT/Extension/Turbo.pm
index 7ef5f4c..13e21d5 100644
--- a/lib/RT/Extension/Turbo.pm
+++ b/lib/RT/Extension/Turbo.pm
@@ -45,6 +45,36 @@ Add this line:
=back
+=head1 ToDo
+
+=over
+
+=item *
+
+Make it a user-level and global config option to switch homepages
+
+=item *
+
+Add a timer at debug level in logs per component
+
+=item *
+
+Fix issue with reloading dropdowns on refresh
+
+=item *
+
+Get forms working (Quickcreate)
+
+=item *
+
+Add auto-refresh feature per portlet
+
+=item *
+
+Add a loading icon for possible slow portlet loads
+
+=back
+
=head1 AUTHOR
Best Practical Solutions, LLC E<lt>modules at bestpractical.comE<gt>
commit 26adf630175e69bed85952ee309b44736d44809d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Mon Aug 1 15:09:55 2022 -0400
Render components and saved searches with turbo
diff --git a/html/Callbacks/RT-Extension-Turbo/autohandler/Final b/html/Callbacks/RT-Extension-Turbo/autohandler/Final
new file mode 100644
index 0000000..0e8e714
--- /dev/null
+++ b/html/Callbacks/RT-Extension-Turbo/autohandler/Final
@@ -0,0 +1,8 @@
+<%init>
+
+# The only thing after this callback is the default RT footer
+# Abort here for Views requests to avoid outputting the footer
+if ( $m->request_path =~ /^\/Views/ ) {
+ $m->abort;
+}
+</%init>
diff --git a/html/Elements/MyTurbo b/html/Elements/MyTurbo
index 7731812..e1560ae 100644
--- a/html/Elements/MyTurbo
+++ b/html/Elements/MyTurbo
@@ -1,5 +1,114 @@
<div class="myrt row">
+<div class="<% 'boxcontainer col-md-' . ( $sidebar ? '8' : '12' ) %>">
+% foreach my $entry ( @$body ) {
+% $show_cb->($entry);
+% }
+</div>
-<p>Hello</p>
-
+% if ( $sidebar ) {
+<div class="boxcontainer col-md-4">
+% foreach my $entry ( @$sidebar ) {
+% $show_cb->($entry);
+% }
</div>
+% }
+</div>
+
+<%init>
+# All lifted from MyRT in core RT
+my %allowed_components = map {$_ => 1} @{RT->Config->Get('HomepageComponents')};
+
+my $user = $session{'CurrentUser'}->UserObj;
+unless ( $Portlets ) {
+ my ($system_default) = RT::System->new($session{'CurrentUser'})->Attributes->Named('DefaultDashboard');
+ my $system_default_id = $system_default ? $system_default->Content : 0;
+ my $dashboard_id = $user->Preferences( DefaultDashboard => $system_default_id ) or return;
+
+ # Allow any user to read system default dashboard
+ my $dashboard = RT::Dashboard->new($system_default_id == $dashboard_id ? RT->SystemUser : $session{'CurrentUser'});
+ my ( $ok, $msg ) = $dashboard->LoadById( $dashboard_id );
+ if ( !$ok ) {
+ my $user_msg = loc('Unable to load selected dashboard, it may have been deleted');
+ if ( $dashboard_id == $system_default_id ) {
+ RT->Logger->warn("Unable to load dashboard: $msg");
+ $m->out($m->scomp('/Elements/ListActions', actions => $user_msg));
+ return;
+ }
+ else {
+ my ( $ok, $sys_msg ) = $dashboard->LoadById( $system_default_id );
+ if ( $ok ) {
+ $m->out($m->scomp('/Elements/ListActions', actions => [$user_msg, loc('Setting homepage to system default homepage')]));
+ my ( $ok, $msg ) = $user->DeletePreferences( 'DefaultDashboard' );
+ RT->Logger->error( "Couldn't delete DefaultDashboard of user " . $user->Name . ": $msg" ) unless $ok;
+ }
+ else {
+ RT->Logger->warn("Unable to load dashboard: $msg $sys_msg");
+ $m->out($m->scomp('/Elements/ListActions', actions => $user_msg));
+ return;
+ }
+ }
+ }
+ $Portlets = $dashboard->Panes;
+}
+
+$m->callback( CallbackName => 'MassagePortlets', Portlets => $Portlets );
+
+my ($body, $sidebar) = @{$Portlets}{qw(body sidebar)};
+unless( $body && @$body ) {
+ $body = $sidebar || [];
+ $sidebar = undef;
+}
+$sidebar = undef unless $sidebar && @$sidebar;
+
+my $Rows = $user->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) );
+
+my $show_cb;
+$show_cb = sub {
+ my $entry = shift;
+ my $type;
+ my $name;
+
+ # Normal handling for RT 5.0.2 and newer
+ my $depth = shift || 0;
+ Abort("Possible recursive dashboard detected.", SuppressHeader => 1) if $depth > 8;
+
+ $type = $entry->{portlet_type};
+ $name = $entry->{component};
+ if ( $type eq 'component' ) {
+ if (!$allowed_components{$name}) {
+ $m->out( $m->interp->apply_escapes( loc("Invalid portlet [_1]", $name), "h" ) );
+ RT->Logger->info("Invalid portlet $name found on user " . $user->Name . "'s homepage");
+ if ($name eq 'QueueList' && $allowed_components{Quicksearch}) {
+ RT->Logger->warning("You may need to replace the component 'Quicksearch' in the HomepageComponents config with 'QueueList'. See the UPGRADING-4.4 document.");
+ }
+ }
+ else {
+ # Add turbo tags for components
+ $m->out('<turbo-frame id="component-' . lc($name) . '" src="/Views/Component/' . $name . '">');
+ $m->out('</turbo-frame>');
+ }
+ } elsif ( $type eq 'search' ) {
+ # Add turbo tags for saved searches
+ my ($saved_search, $search_name) = RT::Dashboard->ShowSearchName($entry);
+ $m->out('<turbo-frame id="savedsearch-' . lc($search_name) . '" src="/Views/Component/SavedSearch?SavedSearch=' . $search_name . '&Rows=' . $Rows . '">');
+ $m->out('</turbo-frame>');
+ } elsif ( $type eq 'dashboard' ) {
+ my $current_dashboard = RT::Dashboard->new($session{CurrentUser});
+ my ($ok, $msg) = $current_dashboard->LoadById($entry->{id});
+ if (!$ok) {
+ $m->out($msg);
+ return;
+ }
+ my @panes = @{ $current_dashboard->Panes->{$entry->{pane}} || [] };
+ for my $portlet (@panes) {
+ $show_cb->($portlet, $depth + 1);
+ }
+ } else {
+ $RT::Logger->error("unknown portlet type '$type'");
+ }
+};
+
+</%init>
+<%args>
+$Portlets => undef
+</%args>
diff --git a/html/Views/Component/dhandler b/html/Views/Component/dhandler
new file mode 100644
index 0000000..775f220
--- /dev/null
+++ b/html/Views/Component/dhandler
@@ -0,0 +1,25 @@
+% if ( $component_name eq 'SavedSearch' ) {
+<turbo-frame id="savedsearch-<%lc($ARGS{SavedSearch})%>">
+% $m->comp( "/Elements/ShowSearch", %ARGS );
+% } else {
+<turbo-frame id="component-<%$component_name%>">
+% $m->comp( "/Elements/$component_name" );
+% }
+</turbo-frame>
+
+<%init>
+my ($component_name) = $m->dhandler_arg;
+
+if ( $component_name eq 'SavedSearch' ) {
+ # Put Override args in the correct structure
+ $ARGS{Override} = {};
+ foreach my $override ( qw(Rows) ) {
+ if ( $ARGS{$override} ) {
+ $ARGS{Override}->{$override} = $ARGS{$override};
+ delete $ARGS{$override};
+ }
+ }
+}
+</%init>
+<%args>
+</%args>
diff --git a/html/turbo_home.html b/html/turbo_home.html
index 777bcae..17716c5 100644
--- a/html/turbo_home.html
+++ b/html/turbo_home.html
@@ -4,6 +4,5 @@
<& /Elements/MyTurbo &>
<%init>
-warn "Running turbo_home";
my @results;
</%init>
commit 12cb0adbe7dbff6ea2dc8eeaf63e61f3e560b533
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Thu Jul 28 13:52:00 2022 -0400
Load Turbo and create a test home page
diff --git a/META.yml b/META.yml
index 018d6e1..68a1e53 100644
--- a/META.yml
+++ b/META.yml
@@ -16,6 +16,7 @@ meta-spec:
name: RT-Extension-Turbo
no_index:
directory:
+ - html
- inc
- static
requires:
diff --git a/html/Callbacks/RT-Extension-Turbo/index.html/Initial b/html/Callbacks/RT-Extension-Turbo/index.html/Initial
new file mode 100644
index 0000000..7aa375e
--- /dev/null
+++ b/html/Callbacks/RT-Extension-Turbo/index.html/Initial
@@ -0,0 +1,3 @@
+<%init>
+RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."turbo_home.html");
+</%init>
diff --git a/html/Elements/MyTurbo b/html/Elements/MyTurbo
new file mode 100644
index 0000000..7731812
--- /dev/null
+++ b/html/Elements/MyTurbo
@@ -0,0 +1,5 @@
+<div class="myrt row">
+
+<p>Hello</p>
+
+</div>
diff --git a/html/turbo_home.html b/html/turbo_home.html
new file mode 100644
index 0000000..777bcae
--- /dev/null
+++ b/html/turbo_home.html
@@ -0,0 +1,9 @@
+<& /Elements/Header, Title => "RT at a glance" &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+<& /Elements/MyTurbo &>
+
+<%init>
+warn "Running turbo_home";
+my @results;
+</%init>
diff --git a/lib/RT/Extension/Turbo.pm b/lib/RT/Extension/Turbo.pm
index 5a7e693..7ef5f4c 100644
--- a/lib/RT/Extension/Turbo.pm
+++ b/lib/RT/Extension/Turbo.pm
@@ -4,6 +4,9 @@ package RT::Extension::Turbo;
our $VERSION = '0.01';
+RT->AddJavaScript('turbo.es2017-umd.js');
+RT->AddJavaScript('rt-extension-turbo.js');
+
=head1 NAME
RT-Extension-Turbo - Experimental extension using Turbo
diff --git a/static/js/rt-extension-turbo.js b/static/js/rt-extension-turbo.js
new file mode 100644
index 0000000..38355bf
--- /dev/null
+++ b/static/js/rt-extension-turbo.js
@@ -0,0 +1,2 @@
+// Disable by default, then enable where needed
+Turbo.session.drive = false;
diff --git a/static/js/turbo.es2017-umd.js b/static/js/turbo.es2017-umd.js
new file mode 100644
index 0000000..07f69ed
--- /dev/null
+++ b/static/js/turbo.es2017-umd.js
@@ -0,0 +1,3362 @@
+/*
+Turbo 7.1.0
+Copyright © 2022 Basecamp, LLC
+ */
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Turbo = {}));
+}(this, (function (exports) { 'use strict';
+
+ (function () {
+ if (window.Reflect === undefined || window.customElements === undefined ||
+ window.customElements.polyfillWrapFlushCallback) {
+ return;
+ }
+ const BuiltInHTMLElement = HTMLElement;
+ const wrapperForTheName = {
+ 'HTMLElement': function HTMLElement() {
+ return Reflect.construct(BuiltInHTMLElement, [], this.constructor);
+ }
+ };
+ window.HTMLElement =
+ wrapperForTheName['HTMLElement'];
+ HTMLElement.prototype = BuiltInHTMLElement.prototype;
+ HTMLElement.prototype.constructor = HTMLElement;
+ Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
+ })();
+
+ /**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2019 Javan Makhmali
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ (function(prototype) {
+ if (typeof prototype.requestSubmit == "function") return
+
+ prototype.requestSubmit = function(submitter) {
+ if (submitter) {
+ validateSubmitter(submitter, this);
+ submitter.click();
+ } else {
+ submitter = document.createElement("input");
+ submitter.type = "submit";
+ submitter.hidden = true;
+ this.appendChild(submitter);
+ submitter.click();
+ this.removeChild(submitter);
+ }
+ };
+
+ function validateSubmitter(submitter, form) {
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
+ }
+
+ function raise(errorConstructor, message, name) {
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
+ }
+ })(HTMLFormElement.prototype);
+
+ const submittersByForm = new WeakMap;
+ function findSubmitterFromClickTarget(target) {
+ const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
+ const candidate = element ? element.closest("input, button") : null;
+ return (candidate === null || candidate === void 0 ? void 0 : candidate.type) == "submit" ? candidate : null;
+ }
+ function clickCaptured(event) {
+ const submitter = findSubmitterFromClickTarget(event.target);
+ if (submitter && submitter.form) {
+ submittersByForm.set(submitter.form, submitter);
+ }
+ }
+ (function () {
+ if ("submitter" in Event.prototype)
+ return;
+ let prototype;
+ if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
+ prototype = window.SubmitEvent.prototype;
+ }
+ else if ("SubmitEvent" in window) {
+ return;
+ }
+ else {
+ prototype = window.Event.prototype;
+ }
+ addEventListener("click", clickCaptured, true);
+ Object.defineProperty(prototype, "submitter", {
+ get() {
+ if (this.type == "submit" && this.target instanceof HTMLFormElement) {
+ return submittersByForm.get(this.target);
+ }
+ }
+ });
+ })();
+
+ var FrameLoadingStyle;
+ (function (FrameLoadingStyle) {
+ FrameLoadingStyle["eager"] = "eager";
+ FrameLoadingStyle["lazy"] = "lazy";
+ })(FrameLoadingStyle || (FrameLoadingStyle = {}));
+ class FrameElement extends HTMLElement {
+ constructor() {
+ super();
+ this.loaded = Promise.resolve();
+ this.delegate = new FrameElement.delegateConstructor(this);
+ }
+ static get observedAttributes() {
+ return ["disabled", "loading", "src"];
+ }
+ connectedCallback() {
+ this.delegate.connect();
+ }
+ disconnectedCallback() {
+ this.delegate.disconnect();
+ }
+ reload() {
+ const { src } = this;
+ this.src = null;
+ this.src = src;
+ }
+ attributeChangedCallback(name) {
+ if (name == "loading") {
+ this.delegate.loadingStyleChanged();
+ }
+ else if (name == "src") {
+ this.delegate.sourceURLChanged();
+ }
+ else {
+ this.delegate.disabledChanged();
+ }
+ }
+ get src() {
+ return this.getAttribute("src");
+ }
+ set src(value) {
+ if (value) {
+ this.setAttribute("src", value);
+ }
+ else {
+ this.removeAttribute("src");
+ }
+ }
+ get loading() {
+ return frameLoadingStyleFromString(this.getAttribute("loading") || "");
+ }
+ set loading(value) {
+ if (value) {
+ this.setAttribute("loading", value);
+ }
+ else {
+ this.removeAttribute("loading");
+ }
+ }
+ get disabled() {
+ return this.hasAttribute("disabled");
+ }
+ set disabled(value) {
+ if (value) {
+ this.setAttribute("disabled", "");
+ }
+ else {
+ this.removeAttribute("disabled");
+ }
+ }
+ get autoscroll() {
+ return this.hasAttribute("autoscroll");
+ }
+ set autoscroll(value) {
+ if (value) {
+ this.setAttribute("autoscroll", "");
+ }
+ else {
+ this.removeAttribute("autoscroll");
+ }
+ }
+ get complete() {
+ return !this.delegate.isLoading;
+ }
+ get isActive() {
+ return this.ownerDocument === document && !this.isPreview;
+ }
+ get isPreview() {
+ var _a, _b;
+ return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview");
+ }
+ }
+ function frameLoadingStyleFromString(style) {
+ switch (style.toLowerCase()) {
+ case "lazy": return FrameLoadingStyle.lazy;
+ default: return FrameLoadingStyle.eager;
+ }
+ }
+
+ function expandURL(locatable) {
+ return new URL(locatable.toString(), document.baseURI);
+ }
+ function getAnchor(url) {
+ let anchorMatch;
+ if (url.hash) {
+ return url.hash.slice(1);
+ }
+ else if (anchorMatch = url.href.match(/#(.*)$/)) {
+ return anchorMatch[1];
+ }
+ }
+ function getAction(form, submitter) {
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
+ return expandURL(action);
+ }
+ function getExtension(url) {
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
+ }
+ function isHTML(url) {
+ return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
+ }
+ function isPrefixedBy(baseURL, url) {
+ const prefix = getPrefix(url);
+ return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
+ }
+ function locationIsVisitable(location, rootLocation) {
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
+ }
+ function getRequestURL(url) {
+ const anchor = getAnchor(url);
+ return anchor != null
+ ? url.href.slice(0, -(anchor.length + 1))
+ : url.href;
+ }
+ function toCacheKey(url) {
+ return getRequestURL(url);
+ }
+ function urlsAreEqual(left, right) {
+ return expandURL(left).href == expandURL(right).href;
+ }
+ function getPathComponents(url) {
+ return url.pathname.split("/").slice(1);
+ }
+ function getLastPathComponent(url) {
+ return getPathComponents(url).slice(-1)[0];
+ }
+ function getPrefix(url) {
+ return addTrailingSlash(url.origin + url.pathname);
+ }
+ function addTrailingSlash(value) {
+ return value.endsWith("/") ? value : value + "/";
+ }
+
+ class FetchResponse {
+ constructor(response) {
+ this.response = response;
+ }
+ get succeeded() {
+ return this.response.ok;
+ }
+ get failed() {
+ return !this.succeeded;
+ }
+ get clientError() {
+ return this.statusCode >= 400 && this.statusCode <= 499;
+ }
+ get serverError() {
+ return this.statusCode >= 500 && this.statusCode <= 599;
+ }
+ get redirected() {
+ return this.response.redirected;
+ }
+ get location() {
+ return expandURL(this.response.url);
+ }
+ get isHTML() {
+ return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
+ }
+ get statusCode() {
+ return this.response.status;
+ }
+ get contentType() {
+ return this.header("Content-Type");
+ }
+ get responseText() {
+ return this.response.clone().text();
+ }
+ get responseHTML() {
+ if (this.isHTML) {
+ return this.response.clone().text();
+ }
+ else {
+ return Promise.resolve(undefined);
+ }
+ }
+ header(name) {
+ return this.response.headers.get(name);
+ }
+ }
+
+ function dispatch(eventName, { target, cancelable, detail } = {}) {
+ const event = new CustomEvent(eventName, { cancelable, bubbles: true, detail });
+ if (target && target.isConnected) {
+ target.dispatchEvent(event);
+ }
+ else {
+ document.documentElement.dispatchEvent(event);
+ }
+ return event;
+ }
+ function nextAnimationFrame() {
+ return new Promise(resolve => requestAnimationFrame(() => resolve()));
+ }
+ function nextEventLoopTick() {
+ return new Promise(resolve => setTimeout(() => resolve(), 0));
+ }
+ function nextMicrotask() {
+ return Promise.resolve();
+ }
+ function parseHTMLDocument(html = "") {
+ return new DOMParser().parseFromString(html, "text/html");
+ }
+ function unindent(strings, ...values) {
+ const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
+ const match = lines[0].match(/^\s+/);
+ const indent = match ? match[0].length : 0;
+ return lines.map(line => line.slice(indent)).join("\n");
+ }
+ function interpolate(strings, values) {
+ return strings.reduce((result, string, i) => {
+ const value = values[i] == undefined ? "" : values[i];
+ return result + string + value;
+ }, "");
+ }
+ function uuid() {
+ return Array.apply(null, { length: 36 }).map((_, i) => {
+ if (i == 8 || i == 13 || i == 18 || i == 23) {
+ return "-";
+ }
+ else if (i == 14) {
+ return "4";
+ }
+ else if (i == 19) {
+ return (Math.floor(Math.random() * 4) + 8).toString(16);
+ }
+ else {
+ return Math.floor(Math.random() * 15).toString(16);
+ }
+ }).join("");
+ }
+ function getAttribute(attributeName, ...elements) {
+ for (const value of elements.map(element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName))) {
+ if (typeof value == "string")
+ return value;
+ }
+ return null;
+ }
+ function markAsBusy(...elements) {
+ for (const element of elements) {
+ if (element.localName == "turbo-frame") {
+ element.setAttribute("busy", "");
+ }
+ element.setAttribute("aria-busy", "true");
+ }
+ }
+ function clearBusyState(...elements) {
+ for (const element of elements) {
+ if (element.localName == "turbo-frame") {
+ element.removeAttribute("busy");
+ }
+ element.removeAttribute("aria-busy");
+ }
+ }
+
+ var FetchMethod;
+ (function (FetchMethod) {
+ FetchMethod[FetchMethod["get"] = 0] = "get";
+ FetchMethod[FetchMethod["post"] = 1] = "post";
+ FetchMethod[FetchMethod["put"] = 2] = "put";
+ FetchMethod[FetchMethod["patch"] = 3] = "patch";
+ FetchMethod[FetchMethod["delete"] = 4] = "delete";
+ })(FetchMethod || (FetchMethod = {}));
+ function fetchMethodFromString(method) {
+ switch (method.toLowerCase()) {
+ case "get": return FetchMethod.get;
+ case "post": return FetchMethod.post;
+ case "put": return FetchMethod.put;
+ case "patch": return FetchMethod.patch;
+ case "delete": return FetchMethod.delete;
+ }
+ }
+ class FetchRequest {
+ constructor(delegate, method, location, body = new URLSearchParams, target = null) {
+ this.abortController = new AbortController;
+ this.resolveRequestPromise = (value) => { };
+ this.delegate = delegate;
+ this.method = method;
+ this.headers = this.defaultHeaders;
+ this.body = body;
+ this.url = location;
+ this.target = target;
+ }
+ get location() {
+ return this.url;
+ }
+ get params() {
+ return this.url.searchParams;
+ }
+ get entries() {
+ return this.body ? Array.from(this.body.entries()) : [];
+ }
+ cancel() {
+ this.abortController.abort();
+ }
+ async perform() {
+ var _a, _b;
+ const { fetchOptions } = this;
+ (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
+ await this.allowRequestToBeIntercepted(fetchOptions);
+ try {
+ this.delegate.requestStarted(this);
+ const response = await fetch(this.url.href, fetchOptions);
+ return await this.receive(response);
+ }
+ catch (error) {
+ if (error.name !== 'AbortError') {
+ this.delegate.requestErrored(this, error);
+ throw error;
+ }
+ }
+ finally {
+ this.delegate.requestFinished(this);
+ }
+ }
+ async receive(response) {
+ const fetchResponse = new FetchResponse(response);
+ const event = dispatch("turbo:before-fetch-response", { cancelable: true, detail: { fetchResponse }, target: this.target });
+ if (event.defaultPrevented) {
+ this.delegate.requestPreventedHandlingResponse(this, fetchResponse);
+ }
+ else if (fetchResponse.succeeded) {
+ this.delegate.requestSucceededWithResponse(this, fetchResponse);
+ }
+ else {
+ this.delegate.requestFailedWithResponse(this, fetchResponse);
+ }
+ return fetchResponse;
+ }
+ get fetchOptions() {
+ var _a;
+ return {
+ method: FetchMethod[this.method].toUpperCase(),
+ credentials: "same-origin",
+ headers: this.headers,
+ redirect: "follow",
+ body: this.isIdempotent ? null : this.body,
+ signal: this.abortSignal,
+ referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
+ };
+ }
+ get defaultHeaders() {
+ return {
+ "Accept": "text/html, application/xhtml+xml"
+ };
+ }
+ get isIdempotent() {
+ return this.method == FetchMethod.get;
+ }
+ get abortSignal() {
+ return this.abortController.signal;
+ }
+ async allowRequestToBeIntercepted(fetchOptions) {
+ const requestInterception = new Promise(resolve => this.resolveRequestPromise = resolve);
+ const event = dispatch("turbo:before-fetch-request", {
+ cancelable: true,
+ detail: {
+ fetchOptions,
+ url: this.url,
+ resume: this.resolveRequestPromise
+ },
+ target: this.target
+ });
+ if (event.defaultPrevented)
+ await requestInterception;
+ }
+ }
+
+ class AppearanceObserver {
+ constructor(delegate, element) {
+ this.started = false;
+ this.intersect = entries => {
+ const lastEntry = entries.slice(-1)[0];
+ if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) {
+ this.delegate.elementAppearedInViewport(this.element);
+ }
+ };
+ this.delegate = delegate;
+ this.element = element;
+ this.intersectionObserver = new IntersectionObserver(this.intersect);
+ }
+ start() {
+ if (!this.started) {
+ this.started = true;
+ this.intersectionObserver.observe(this.element);
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.started = false;
+ this.intersectionObserver.unobserve(this.element);
+ }
+ }
+ }
+
+ class StreamMessage {
+ constructor(html) {
+ this.templateElement = document.createElement("template");
+ this.templateElement.innerHTML = html;
+ }
+ static wrap(message) {
+ if (typeof message == "string") {
+ return new this(message);
+ }
+ else {
+ return message;
+ }
+ }
+ get fragment() {
+ const fragment = document.createDocumentFragment();
+ for (const element of this.foreignElements) {
+ fragment.appendChild(document.importNode(element, true));
+ }
+ return fragment;
+ }
+ get foreignElements() {
+ return this.templateChildren.reduce((streamElements, child) => {
+ if (child.tagName.toLowerCase() == "turbo-stream") {
+ return [...streamElements, child];
+ }
+ else {
+ return streamElements;
+ }
+ }, []);
+ }
+ get templateChildren() {
+ return Array.from(this.templateElement.content.children);
+ }
+ }
+ StreamMessage.contentType = "text/vnd.turbo-stream.html";
+
+ var FormSubmissionState;
+ (function (FormSubmissionState) {
+ FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
+ FormSubmissionState[FormSubmissionState["requesting"] = 1] = "requesting";
+ FormSubmissionState[FormSubmissionState["waiting"] = 2] = "waiting";
+ FormSubmissionState[FormSubmissionState["receiving"] = 3] = "receiving";
+ FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
+ FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
+ })(FormSubmissionState || (FormSubmissionState = {}));
+ var FormEnctype;
+ (function (FormEnctype) {
+ FormEnctype["urlEncoded"] = "application/x-www-form-urlencoded";
+ FormEnctype["multipart"] = "multipart/form-data";
+ FormEnctype["plain"] = "text/plain";
+ })(FormEnctype || (FormEnctype = {}));
+ function formEnctypeFromString(encoding) {
+ switch (encoding.toLowerCase()) {
+ case FormEnctype.multipart: return FormEnctype.multipart;
+ case FormEnctype.plain: return FormEnctype.plain;
+ default: return FormEnctype.urlEncoded;
+ }
+ }
+ class FormSubmission {
+ constructor(delegate, formElement, submitter, mustRedirect = false) {
+ this.state = FormSubmissionState.initialized;
+ this.delegate = delegate;
+ this.formElement = formElement;
+ this.submitter = submitter;
+ this.formData = buildFormData(formElement, submitter);
+ this.location = expandURL(this.action);
+ if (this.method == FetchMethod.get) {
+ mergeFormDataEntries(this.location, [...this.body.entries()]);
+ }
+ this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
+ this.mustRedirect = mustRedirect;
+ }
+ static confirmMethod(message, element) {
+ return confirm(message);
+ }
+ get method() {
+ var _a;
+ const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
+ return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
+ }
+ get action() {
+ var _a;
+ const formElementAction = typeof this.formElement.action === 'string' ? this.formElement.action : null;
+ return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
+ }
+ get body() {
+ if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
+ return new URLSearchParams(this.stringFormData);
+ }
+ else {
+ return this.formData;
+ }
+ }
+ get enctype() {
+ var _a;
+ return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
+ }
+ get isIdempotent() {
+ return this.fetchRequest.isIdempotent;
+ }
+ get stringFormData() {
+ return [...this.formData].reduce((entries, [name, value]) => {
+ return entries.concat(typeof value == "string" ? [[name, value]] : []);
+ }, []);
+ }
+ get confirmationMessage() {
+ return this.formElement.getAttribute("data-turbo-confirm");
+ }
+ get needsConfirmation() {
+ return this.confirmationMessage !== null;
+ }
+ async start() {
+ const { initialized, requesting } = FormSubmissionState;
+ if (this.needsConfirmation) {
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
+ if (!answer) {
+ return;
+ }
+ }
+ if (this.state == initialized) {
+ this.state = requesting;
+ return this.fetchRequest.perform();
+ }
+ }
+ stop() {
+ const { stopping, stopped } = FormSubmissionState;
+ if (this.state != stopping && this.state != stopped) {
+ this.state = stopping;
+ this.fetchRequest.cancel();
+ return true;
+ }
+ }
+ prepareHeadersForRequest(headers, request) {
+ if (!request.isIdempotent) {
+ const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
+ if (token) {
+ headers["X-CSRF-Token"] = token;
+ }
+ headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ");
+ }
+ }
+ requestStarted(request) {
+ var _a;
+ this.state = FormSubmissionState.waiting;
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
+ dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
+ this.delegate.formSubmissionStarted(this);
+ }
+ requestPreventedHandlingResponse(request, response) {
+ this.result = { success: response.succeeded, fetchResponse: response };
+ }
+ requestSucceededWithResponse(request, response) {
+ if (response.clientError || response.serverError) {
+ this.delegate.formSubmissionFailedWithResponse(this, response);
+ }
+ else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
+ const error = new Error("Form responses must redirect to another location");
+ this.delegate.formSubmissionErrored(this, error);
+ }
+ else {
+ this.state = FormSubmissionState.receiving;
+ this.result = { success: true, fetchResponse: response };
+ this.delegate.formSubmissionSucceededWithResponse(this, response);
+ }
+ }
+ requestFailedWithResponse(request, response) {
+ this.result = { success: false, fetchResponse: response };
+ this.delegate.formSubmissionFailedWithResponse(this, response);
+ }
+ requestErrored(request, error) {
+ this.result = { success: false, error };
+ this.delegate.formSubmissionErrored(this, error);
+ }
+ requestFinished(request) {
+ var _a;
+ this.state = FormSubmissionState.stopped;
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
+ dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
+ this.delegate.formSubmissionFinished(this);
+ }
+ requestMustRedirect(request) {
+ return !request.isIdempotent && this.mustRedirect;
+ }
+ }
+ function buildFormData(formElement, submitter) {
+ const formData = new FormData(formElement);
+ const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
+ const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
+ if (name && value != null && formData.get(name) != value) {
+ formData.append(name, value);
+ }
+ return formData;
+ }
+ function getCookieValue(cookieName) {
+ if (cookieName != null) {
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
+ const cookie = cookies.find((cookie) => cookie.startsWith(cookieName));
+ if (cookie) {
+ const value = cookie.split("=").slice(1).join("=");
+ return value ? decodeURIComponent(value) : undefined;
+ }
+ }
+ }
+ function getMetaContent(name) {
+ const element = document.querySelector(`meta[name="${name}"]`);
+ return element && element.content;
+ }
+ function responseSucceededWithoutRedirect(response) {
+ return response.statusCode == 200 && !response.redirected;
+ }
+ function mergeFormDataEntries(url, entries) {
+ const searchParams = new URLSearchParams;
+ for (const [name, value] of entries) {
+ if (value instanceof File)
+ continue;
+ searchParams.append(name, value);
+ }
+ url.search = searchParams.toString();
+ return url;
+ }
+
+ class Snapshot {
+ constructor(element) {
+ this.element = element;
+ }
+ get children() {
+ return [...this.element.children];
+ }
+ hasAnchor(anchor) {
+ return this.getElementForAnchor(anchor) != null;
+ }
+ getElementForAnchor(anchor) {
+ return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null;
+ }
+ get isConnected() {
+ return this.element.isConnected;
+ }
+ get firstAutofocusableElement() {
+ return this.element.querySelector("[autofocus]");
+ }
+ get permanentElements() {
+ return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
+ }
+ getPermanentElementById(id) {
+ return this.element.querySelector(`#${id}[data-turbo-permanent]`);
+ }
+ getPermanentElementMapForSnapshot(snapshot) {
+ const permanentElementMap = {};
+ for (const currentPermanentElement of this.permanentElements) {
+ const { id } = currentPermanentElement;
+ const newPermanentElement = snapshot.getPermanentElementById(id);
+ if (newPermanentElement) {
+ permanentElementMap[id] = [currentPermanentElement, newPermanentElement];
+ }
+ }
+ return permanentElementMap;
+ }
+ }
+
+ class FormInterceptor {
+ constructor(delegate, element) {
+ this.submitBubbled = ((event) => {
+ const form = event.target;
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
+ const submitter = event.submitter || undefined;
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ this.delegate.formSubmissionIntercepted(form, submitter);
+ }
+ }
+ });
+ this.delegate = delegate;
+ this.element = element;
+ }
+ start() {
+ this.element.addEventListener("submit", this.submitBubbled);
+ }
+ stop() {
+ this.element.removeEventListener("submit", this.submitBubbled);
+ }
+ }
+
+ class View {
+ constructor(delegate, element) {
+ this.resolveRenderPromise = (value) => { };
+ this.resolveInterceptionPromise = (value) => { };
+ this.delegate = delegate;
+ this.element = element;
+ }
+ scrollToAnchor(anchor) {
+ const element = this.snapshot.getElementForAnchor(anchor);
+ if (element) {
+ this.scrollToElement(element);
+ this.focusElement(element);
+ }
+ else {
+ this.scrollToPosition({ x: 0, y: 0 });
+ }
+ }
+ scrollToAnchorFromLocation(location) {
+ this.scrollToAnchor(getAnchor(location));
+ }
+ scrollToElement(element) {
+ element.scrollIntoView();
+ }
+ focusElement(element) {
+ if (element instanceof HTMLElement) {
+ if (element.hasAttribute("tabindex")) {
+ element.focus();
+ }
+ else {
+ element.setAttribute("tabindex", "-1");
+ element.focus();
+ element.removeAttribute("tabindex");
+ }
+ }
+ }
+ scrollToPosition({ x, y }) {
+ this.scrollRoot.scrollTo(x, y);
+ }
+ scrollToTop() {
+ this.scrollToPosition({ x: 0, y: 0 });
+ }
+ get scrollRoot() {
+ return window;
+ }
+ async render(renderer) {
+ const { isPreview, shouldRender, newSnapshot: snapshot } = renderer;
+ if (shouldRender) {
+ try {
+ this.renderPromise = new Promise(resolve => this.resolveRenderPromise = resolve);
+ this.renderer = renderer;
+ this.prepareToRenderSnapshot(renderer);
+ const renderInterception = new Promise(resolve => this.resolveInterceptionPromise = resolve);
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
+ if (!immediateRender)
+ await renderInterception;
+ await this.renderSnapshot(renderer);
+ this.delegate.viewRenderedSnapshot(snapshot, isPreview);
+ this.finishRenderingSnapshot(renderer);
+ }
+ finally {
+ delete this.renderer;
+ this.resolveRenderPromise(undefined);
+ delete this.renderPromise;
+ }
+ }
+ else {
+ this.invalidate();
+ }
+ }
+ invalidate() {
+ this.delegate.viewInvalidated();
+ }
+ prepareToRenderSnapshot(renderer) {
+ this.markAsPreview(renderer.isPreview);
+ renderer.prepareToRender();
+ }
+ markAsPreview(isPreview) {
+ if (isPreview) {
+ this.element.setAttribute("data-turbo-preview", "");
+ }
+ else {
+ this.element.removeAttribute("data-turbo-preview");
+ }
+ }
+ async renderSnapshot(renderer) {
+ await renderer.render();
+ }
+ finishRenderingSnapshot(renderer) {
+ renderer.finishRendering();
+ }
+ }
+
+ class FrameView extends View {
+ invalidate() {
+ this.element.innerHTML = "";
+ }
+ get snapshot() {
+ return new Snapshot(this.element);
+ }
+ }
+
+ class LinkInterceptor {
+ constructor(delegate, element) {
+ this.clickBubbled = (event) => {
+ if (this.respondsToEventTarget(event.target)) {
+ this.clickEvent = event;
+ }
+ else {
+ delete this.clickEvent;
+ }
+ };
+ this.linkClicked = ((event) => {
+ if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
+ if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
+ this.clickEvent.preventDefault();
+ event.preventDefault();
+ this.delegate.linkClickIntercepted(event.target, event.detail.url);
+ }
+ }
+ delete this.clickEvent;
+ });
+ this.willVisit = () => {
+ delete this.clickEvent;
+ };
+ this.delegate = delegate;
+ this.element = element;
+ }
+ start() {
+ this.element.addEventListener("click", this.clickBubbled);
+ document.addEventListener("turbo:click", this.linkClicked);
+ document.addEventListener("turbo:before-visit", this.willVisit);
+ }
+ stop() {
+ this.element.removeEventListener("click", this.clickBubbled);
+ document.removeEventListener("turbo:click", this.linkClicked);
+ document.removeEventListener("turbo:before-visit", this.willVisit);
+ }
+ respondsToEventTarget(target) {
+ const element = target instanceof Element
+ ? target
+ : target instanceof Node
+ ? target.parentElement
+ : null;
+ return element && element.closest("turbo-frame, html") == this.element;
+ }
+ }
+
+ class Bardo {
+ constructor(permanentElementMap) {
+ this.permanentElementMap = permanentElementMap;
+ }
+ static preservingPermanentElements(permanentElementMap, callback) {
+ const bardo = new this(permanentElementMap);
+ bardo.enter();
+ callback();
+ bardo.leave();
+ }
+ enter() {
+ for (const id in this.permanentElementMap) {
+ const [, newPermanentElement] = this.permanentElementMap[id];
+ this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
+ }
+ }
+ leave() {
+ for (const id in this.permanentElementMap) {
+ const [currentPermanentElement] = this.permanentElementMap[id];
+ this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
+ this.replacePlaceholderWithPermanentElement(currentPermanentElement);
+ }
+ }
+ replaceNewPermanentElementWithPlaceholder(permanentElement) {
+ const placeholder = createPlaceholderForPermanentElement(permanentElement);
+ permanentElement.replaceWith(placeholder);
+ }
+ replaceCurrentPermanentElementWithClone(permanentElement) {
+ const clone = permanentElement.cloneNode(true);
+ permanentElement.replaceWith(clone);
+ }
+ replacePlaceholderWithPermanentElement(permanentElement) {
+ const placeholder = this.getPlaceholderById(permanentElement.id);
+ placeholder === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement);
+ }
+ getPlaceholderById(id) {
+ return this.placeholders.find(element => element.content == id);
+ }
+ get placeholders() {
+ return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")];
+ }
+ }
+ function createPlaceholderForPermanentElement(permanentElement) {
+ const element = document.createElement("meta");
+ element.setAttribute("name", "turbo-permanent-placeholder");
+ element.setAttribute("content", permanentElement.id);
+ return element;
+ }
+
+ class Renderer {
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
+ this.currentSnapshot = currentSnapshot;
+ this.newSnapshot = newSnapshot;
+ this.isPreview = isPreview;
+ this.willRender = willRender;
+ this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
+ }
+ get shouldRender() {
+ return true;
+ }
+ prepareToRender() {
+ return;
+ }
+ finishRendering() {
+ if (this.resolvingFunctions) {
+ this.resolvingFunctions.resolve();
+ delete this.resolvingFunctions;
+ }
+ }
+ createScriptElement(element) {
+ if (element.getAttribute("data-turbo-eval") == "false") {
+ return element;
+ }
+ else {
+ const createdScriptElement = document.createElement("script");
+ if (this.cspNonce) {
+ createdScriptElement.nonce = this.cspNonce;
+ }
+ createdScriptElement.textContent = element.textContent;
+ createdScriptElement.async = false;
+ copyElementAttributes(createdScriptElement, element);
+ return createdScriptElement;
+ }
+ }
+ preservingPermanentElements(callback) {
+ Bardo.preservingPermanentElements(this.permanentElementMap, callback);
+ }
+ focusFirstAutofocusableElement() {
+ const element = this.connectedSnapshot.firstAutofocusableElement;
+ if (elementIsFocusable(element)) {
+ element.focus();
+ }
+ }
+ get connectedSnapshot() {
+ return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
+ }
+ get currentElement() {
+ return this.currentSnapshot.element;
+ }
+ get newElement() {
+ return this.newSnapshot.element;
+ }
+ get permanentElementMap() {
+ return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
+ }
+ get cspNonce() {
+ var _a;
+ return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
+ }
+ }
+ function copyElementAttributes(destinationElement, sourceElement) {
+ for (const { name, value } of [...sourceElement.attributes]) {
+ destinationElement.setAttribute(name, value);
+ }
+ }
+ function elementIsFocusable(element) {
+ return element && typeof element.focus == "function";
+ }
+
+ class FrameRenderer extends Renderer {
+ get shouldRender() {
+ return true;
+ }
+ async render() {
+ await nextAnimationFrame();
+ this.preservingPermanentElements(() => {
+ this.loadFrameElement();
+ });
+ this.scrollFrameIntoView();
+ await nextAnimationFrame();
+ this.focusFirstAutofocusableElement();
+ await nextAnimationFrame();
+ this.activateScriptElements();
+ }
+ loadFrameElement() {
+ var _a;
+ const destinationRange = document.createRange();
+ destinationRange.selectNodeContents(this.currentElement);
+ destinationRange.deleteContents();
+ const frameElement = this.newElement;
+ const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
+ if (sourceRange) {
+ sourceRange.selectNodeContents(frameElement);
+ this.currentElement.appendChild(sourceRange.extractContents());
+ }
+ }
+ scrollFrameIntoView() {
+ if (this.currentElement.autoscroll || this.newElement.autoscroll) {
+ const element = this.currentElement.firstElementChild;
+ const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
+ if (element) {
+ element.scrollIntoView({ block });
+ return true;
+ }
+ }
+ return false;
+ }
+ activateScriptElements() {
+ for (const inertScriptElement of this.newScriptElements) {
+ const activatedScriptElement = this.createScriptElement(inertScriptElement);
+ inertScriptElement.replaceWith(activatedScriptElement);
+ }
+ }
+ get newScriptElements() {
+ return this.currentElement.querySelectorAll("script");
+ }
+ }
+ function readScrollLogicalPosition(value, defaultValue) {
+ if (value == "end" || value == "start" || value == "center" || value == "nearest") {
+ return value;
+ }
+ else {
+ return defaultValue;
+ }
+ }
+
+ class ProgressBar {
+ constructor() {
+ this.hiding = false;
+ this.value = 0;
+ this.visible = false;
+ this.trickle = () => {
+ this.setValue(this.value + Math.random() / 100);
+ };
+ this.stylesheetElement = this.createStylesheetElement();
+ this.progressElement = this.createProgressElement();
+ this.installStylesheetElement();
+ this.setValue(0);
+ }
+ static get defaultCSS() {
+ return unindent `
+ .turbo-progress-bar {
+ position: fixed;
+ display: block;
+ top: 0;
+ left: 0;
+ height: 3px;
+ background: #0076ff;
+ z-index: 9999;
+ transition:
+ width ${ProgressBar.animationDuration}ms ease-out,
+ opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
+ transform: translate3d(0, 0, 0);
+ }
+ `;
+ }
+ show() {
+ if (!this.visible) {
+ this.visible = true;
+ this.installProgressElement();
+ this.startTrickling();
+ }
+ }
+ hide() {
+ if (this.visible && !this.hiding) {
+ this.hiding = true;
+ this.fadeProgressElement(() => {
+ this.uninstallProgressElement();
+ this.stopTrickling();
+ this.visible = false;
+ this.hiding = false;
+ });
+ }
+ }
+ setValue(value) {
+ this.value = value;
+ this.refresh();
+ }
+ installStylesheetElement() {
+ document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
+ }
+ installProgressElement() {
+ this.progressElement.style.width = "0";
+ this.progressElement.style.opacity = "1";
+ document.documentElement.insertBefore(this.progressElement, document.body);
+ this.refresh();
+ }
+ fadeProgressElement(callback) {
+ this.progressElement.style.opacity = "0";
+ setTimeout(callback, ProgressBar.animationDuration * 1.5);
+ }
+ uninstallProgressElement() {
+ if (this.progressElement.parentNode) {
+ document.documentElement.removeChild(this.progressElement);
+ }
+ }
+ startTrickling() {
+ if (!this.trickleInterval) {
+ this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
+ }
+ }
+ stopTrickling() {
+ window.clearInterval(this.trickleInterval);
+ delete this.trickleInterval;
+ }
+ refresh() {
+ requestAnimationFrame(() => {
+ this.progressElement.style.width = `${10 + (this.value * 90)}%`;
+ });
+ }
+ createStylesheetElement() {
+ const element = document.createElement("style");
+ element.type = "text/css";
+ element.textContent = ProgressBar.defaultCSS;
+ return element;
+ }
+ createProgressElement() {
+ const element = document.createElement("div");
+ element.className = "turbo-progress-bar";
+ return element;
+ }
+ }
+ ProgressBar.animationDuration = 300;
+
+ class HeadSnapshot extends Snapshot {
+ constructor() {
+ super(...arguments);
+ this.detailsByOuterHTML = this.children
+ .filter((element) => !elementIsNoscript(element))
+ .map((element) => elementWithoutNonce(element))
+ .reduce((result, element) => {
+ const { outerHTML } = element;
+ const details = outerHTML in result
+ ? result[outerHTML]
+ : {
+ type: elementType(element),
+ tracked: elementIsTracked(element),
+ elements: []
+ };
+ return Object.assign(Object.assign({}, result), { [outerHTML]: Object.assign(Object.assign({}, details), { elements: [...details.elements, element] }) });
+ }, {});
+ }
+ get trackedElementSignature() {
+ return Object.keys(this.detailsByOuterHTML)
+ .filter(outerHTML => this.detailsByOuterHTML[outerHTML].tracked)
+ .join("");
+ }
+ getScriptElementsNotInSnapshot(snapshot) {
+ return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
+ }
+ getStylesheetElementsNotInSnapshot(snapshot) {
+ return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
+ }
+ getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
+ return Object.keys(this.detailsByOuterHTML)
+ .filter(outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))
+ .map(outerHTML => this.detailsByOuterHTML[outerHTML])
+ .filter(({ type }) => type == matchedType)
+ .map(({ elements: [element] }) => element);
+ }
+ get provisionalElements() {
+ return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
+ const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML];
+ if (type == null && !tracked) {
+ return [...result, ...elements];
+ }
+ else if (elements.length > 1) {
+ return [...result, ...elements.slice(1)];
+ }
+ else {
+ return result;
+ }
+ }, []);
+ }
+ getMetaValue(name) {
+ const element = this.findMetaElementByName(name);
+ return element
+ ? element.getAttribute("content")
+ : null;
+ }
+ findMetaElementByName(name) {
+ return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
+ const { elements: [element] } = this.detailsByOuterHTML[outerHTML];
+ return elementIsMetaElementWithName(element, name) ? element : result;
+ }, undefined);
+ }
+ }
+ function elementType(element) {
+ if (elementIsScript(element)) {
+ return "script";
+ }
+ else if (elementIsStylesheet(element)) {
+ return "stylesheet";
+ }
+ }
+ function elementIsTracked(element) {
+ return element.getAttribute("data-turbo-track") == "reload";
+ }
+ function elementIsScript(element) {
+ const tagName = element.tagName.toLowerCase();
+ return tagName == "script";
+ }
+ function elementIsNoscript(element) {
+ const tagName = element.tagName.toLowerCase();
+ return tagName == "noscript";
+ }
+ function elementIsStylesheet(element) {
+ const tagName = element.tagName.toLowerCase();
+ return tagName == "style" || (tagName == "link" && element.getAttribute("rel") == "stylesheet");
+ }
+ function elementIsMetaElementWithName(element, name) {
+ const tagName = element.tagName.toLowerCase();
+ return tagName == "meta" && element.getAttribute("name") == name;
+ }
+ function elementWithoutNonce(element) {
+ if (element.hasAttribute("nonce")) {
+ element.setAttribute("nonce", "");
+ }
+ return element;
+ }
+
+ class PageSnapshot extends Snapshot {
+ constructor(element, headSnapshot) {
+ super(element);
+ this.headSnapshot = headSnapshot;
+ }
+ static fromHTMLString(html = "") {
+ return this.fromDocument(parseHTMLDocument(html));
+ }
+ static fromElement(element) {
+ return this.fromDocument(element.ownerDocument);
+ }
+ static fromDocument({ head, body }) {
+ return new this(body, new HeadSnapshot(head));
+ }
+ clone() {
+ return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
+ }
+ get headElement() {
+ return this.headSnapshot.element;
+ }
+ get rootLocation() {
+ var _a;
+ const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
+ return expandURL(root);
+ }
+ get cacheControlValue() {
+ return this.getSetting("cache-control");
+ }
+ get isPreviewable() {
+ return this.cacheControlValue != "no-preview";
+ }
+ get isCacheable() {
+ return this.cacheControlValue != "no-cache";
+ }
+ get isVisitable() {
+ return this.getSetting("visit-control") != "reload";
+ }
+ getSetting(name) {
+ return this.headSnapshot.getMetaValue(`turbo-${name}`);
+ }
+ }
+
+ var TimingMetric;
+ (function (TimingMetric) {
+ TimingMetric["visitStart"] = "visitStart";
+ TimingMetric["requestStart"] = "requestStart";
+ TimingMetric["requestEnd"] = "requestEnd";
+ TimingMetric["visitEnd"] = "visitEnd";
+ })(TimingMetric || (TimingMetric = {}));
+ var VisitState;
+ (function (VisitState) {
+ VisitState["initialized"] = "initialized";
+ VisitState["started"] = "started";
+ VisitState["canceled"] = "canceled";
+ VisitState["failed"] = "failed";
+ VisitState["completed"] = "completed";
+ })(VisitState || (VisitState = {}));
+ const defaultOptions = {
+ action: "advance",
+ historyChanged: false,
+ visitCachedSnapshot: () => { },
+ willRender: true,
+ };
+ var SystemStatusCode;
+ (function (SystemStatusCode) {
+ SystemStatusCode[SystemStatusCode["networkFailure"] = 0] = "networkFailure";
+ SystemStatusCode[SystemStatusCode["timeoutFailure"] = -1] = "timeoutFailure";
+ SystemStatusCode[SystemStatusCode["contentTypeMismatch"] = -2] = "contentTypeMismatch";
+ })(SystemStatusCode || (SystemStatusCode = {}));
+ class Visit {
+ constructor(delegate, location, restorationIdentifier, options = {}) {
+ this.identifier = uuid();
+ this.timingMetrics = {};
+ this.followedRedirect = false;
+ this.historyChanged = false;
+ this.scrolled = false;
+ this.snapshotCached = false;
+ this.state = VisitState.initialized;
+ this.delegate = delegate;
+ this.location = location;
+ this.restorationIdentifier = restorationIdentifier || uuid();
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
+ this.action = action;
+ this.historyChanged = historyChanged;
+ this.referrer = referrer;
+ this.snapshotHTML = snapshotHTML;
+ this.response = response;
+ this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
+ this.visitCachedSnapshot = visitCachedSnapshot;
+ this.willRender = willRender;
+ this.scrolled = !willRender;
+ }
+ get adapter() {
+ return this.delegate.adapter;
+ }
+ get view() {
+ return this.delegate.view;
+ }
+ get history() {
+ return this.delegate.history;
+ }
+ get restorationData() {
+ return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
+ }
+ get silent() {
+ return this.isSamePage;
+ }
+ start() {
+ if (this.state == VisitState.initialized) {
+ this.recordTimingMetric(TimingMetric.visitStart);
+ this.state = VisitState.started;
+ this.adapter.visitStarted(this);
+ this.delegate.visitStarted(this);
+ }
+ }
+ cancel() {
+ if (this.state == VisitState.started) {
+ if (this.request) {
+ this.request.cancel();
+ }
+ this.cancelRender();
+ this.state = VisitState.canceled;
+ }
+ }
+ complete() {
+ if (this.state == VisitState.started) {
+ this.recordTimingMetric(TimingMetric.visitEnd);
+ this.state = VisitState.completed;
+ this.adapter.visitCompleted(this);
+ this.delegate.visitCompleted(this);
+ this.followRedirect();
+ }
+ }
+ fail() {
+ if (this.state == VisitState.started) {
+ this.state = VisitState.failed;
+ this.adapter.visitFailed(this);
+ }
+ }
+ changeHistory() {
+ var _a;
+ if (!this.historyChanged) {
+ const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
+ const method = this.getHistoryMethodForAction(actionForHistory);
+ this.history.update(method, this.location, this.restorationIdentifier);
+ this.historyChanged = true;
+ }
+ }
+ issueRequest() {
+ if (this.hasPreloadedResponse()) {
+ this.simulateRequest();
+ }
+ else if (this.shouldIssueRequest() && !this.request) {
+ this.request = new FetchRequest(this, FetchMethod.get, this.location);
+ this.request.perform();
+ }
+ }
+ simulateRequest() {
+ if (this.response) {
+ this.startRequest();
+ this.recordResponse();
+ this.finishRequest();
+ }
+ }
+ startRequest() {
+ this.recordTimingMetric(TimingMetric.requestStart);
+ this.adapter.visitRequestStarted(this);
+ }
+ recordResponse(response = this.response) {
+ this.response = response;
+ if (response) {
+ const { statusCode } = response;
+ if (isSuccessful(statusCode)) {
+ this.adapter.visitRequestCompleted(this);
+ }
+ else {
+ this.adapter.visitRequestFailedWithStatusCode(this, statusCode);
+ }
+ }
+ }
+ finishRequest() {
+ this.recordTimingMetric(TimingMetric.requestEnd);
+ this.adapter.visitRequestFinished(this);
+ }
+ loadResponse() {
+ if (this.response) {
+ const { statusCode, responseHTML } = this.response;
+ this.render(async () => {
+ this.cacheSnapshot();
+ if (this.view.renderPromise)
+ await this.view.renderPromise;
+ if (isSuccessful(statusCode) && responseHTML != null) {
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
+ this.adapter.visitRendered(this);
+ this.complete();
+ }
+ else {
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
+ this.adapter.visitRendered(this);
+ this.fail();
+ }
+ });
+ }
+ }
+ getCachedSnapshot() {
+ const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
+ if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
+ if (this.action == "restore" || snapshot.isPreviewable) {
+ return snapshot;
+ }
+ }
+ }
+ getPreloadedSnapshot() {
+ if (this.snapshotHTML) {
+ return PageSnapshot.fromHTMLString(this.snapshotHTML);
+ }
+ }
+ hasCachedSnapshot() {
+ return this.getCachedSnapshot() != null;
+ }
+ loadCachedSnapshot() {
+ const snapshot = this.getCachedSnapshot();
+ if (snapshot) {
+ const isPreview = this.shouldIssueRequest();
+ this.render(async () => {
+ this.cacheSnapshot();
+ if (this.isSamePage) {
+ this.adapter.visitRendered(this);
+ }
+ else {
+ if (this.view.renderPromise)
+ await this.view.renderPromise;
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
+ this.adapter.visitRendered(this);
+ if (!isPreview) {
+ this.complete();
+ }
+ }
+ });
+ }
+ }
+ followRedirect() {
+ var _a;
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
+ this.adapter.visitProposedToLocation(this.redirectedToLocation, {
+ action: 'replace',
+ response: this.response
+ });
+ this.followedRedirect = true;
+ }
+ }
+ goToSamePageAnchor() {
+ if (this.isSamePage) {
+ this.render(async () => {
+ this.cacheSnapshot();
+ this.adapter.visitRendered(this);
+ });
+ }
+ }
+ requestStarted() {
+ this.startRequest();
+ }
+ requestPreventedHandlingResponse(request, response) {
+ }
+ async requestSucceededWithResponse(request, response) {
+ const responseHTML = await response.responseHTML;
+ const { redirected, statusCode } = response;
+ if (responseHTML == undefined) {
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
+ }
+ else {
+ this.redirectedToLocation = response.redirected ? response.location : undefined;
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
+ }
+ }
+ async requestFailedWithResponse(request, response) {
+ const responseHTML = await response.responseHTML;
+ const { redirected, statusCode } = response;
+ if (responseHTML == undefined) {
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
+ }
+ else {
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
+ }
+ }
+ requestErrored(request, error) {
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
+ }
+ requestFinished() {
+ this.finishRequest();
+ }
+ performScroll() {
+ if (!this.scrolled) {
+ if (this.action == "restore") {
+ this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
+ }
+ else {
+ this.scrollToAnchor() || this.view.scrollToTop();
+ }
+ if (this.isSamePage) {
+ this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
+ }
+ this.scrolled = true;
+ }
+ }
+ scrollToRestoredPosition() {
+ const { scrollPosition } = this.restorationData;
+ if (scrollPosition) {
+ this.view.scrollToPosition(scrollPosition);
+ return true;
+ }
+ }
+ scrollToAnchor() {
+ const anchor = getAnchor(this.location);
+ if (anchor != null) {
+ this.view.scrollToAnchor(anchor);
+ return true;
+ }
+ }
+ recordTimingMetric(metric) {
+ this.timingMetrics[metric] = new Date().getTime();
+ }
+ getTimingMetrics() {
+ return Object.assign({}, this.timingMetrics);
+ }
+ getHistoryMethodForAction(action) {
+ switch (action) {
+ case "replace": return history.replaceState;
+ case "advance":
+ case "restore": return history.pushState;
+ }
+ }
+ hasPreloadedResponse() {
+ return typeof this.response == "object";
+ }
+ shouldIssueRequest() {
+ if (this.isSamePage) {
+ return false;
+ }
+ else if (this.action == "restore") {
+ return !this.hasCachedSnapshot();
+ }
+ else {
+ return this.willRender;
+ }
+ }
+ cacheSnapshot() {
+ if (!this.snapshotCached) {
+ this.view.cacheSnapshot().then(snapshot => snapshot && this.visitCachedSnapshot(snapshot));
+ this.snapshotCached = true;
+ }
+ }
+ async render(callback) {
+ this.cancelRender();
+ await new Promise(resolve => {
+ this.frame = requestAnimationFrame(() => resolve());
+ });
+ await callback();
+ delete this.frame;
+ this.performScroll();
+ }
+ cancelRender() {
+ if (this.frame) {
+ cancelAnimationFrame(this.frame);
+ delete this.frame;
+ }
+ }
+ }
+ function isSuccessful(statusCode) {
+ return statusCode >= 200 && statusCode < 300;
+ }
+
+ class BrowserAdapter {
+ constructor(session) {
+ this.progressBar = new ProgressBar;
+ this.showProgressBar = () => {
+ this.progressBar.show();
+ };
+ this.session = session;
+ }
+ visitProposedToLocation(location, options) {
+ this.navigator.startVisit(location, uuid(), options);
+ }
+ visitStarted(visit) {
+ visit.loadCachedSnapshot();
+ visit.issueRequest();
+ visit.changeHistory();
+ visit.goToSamePageAnchor();
+ }
+ visitRequestStarted(visit) {
+ this.progressBar.setValue(0);
+ if (visit.hasCachedSnapshot() || visit.action != "restore") {
+ this.showVisitProgressBarAfterDelay();
+ }
+ else {
+ this.showProgressBar();
+ }
+ }
+ visitRequestCompleted(visit) {
+ visit.loadResponse();
+ }
+ visitRequestFailedWithStatusCode(visit, statusCode) {
+ switch (statusCode) {
+ case SystemStatusCode.networkFailure:
+ case SystemStatusCode.timeoutFailure:
+ case SystemStatusCode.contentTypeMismatch:
+ return this.reload();
+ default:
+ return visit.loadResponse();
+ }
+ }
+ visitRequestFinished(visit) {
+ this.progressBar.setValue(1);
+ this.hideVisitProgressBar();
+ }
+ visitCompleted(visit) {
+ }
+ pageInvalidated() {
+ this.reload();
+ }
+ visitFailed(visit) {
+ }
+ visitRendered(visit) {
+ }
+ formSubmissionStarted(formSubmission) {
+ this.progressBar.setValue(0);
+ this.showFormProgressBarAfterDelay();
+ }
+ formSubmissionFinished(formSubmission) {
+ this.progressBar.setValue(1);
+ this.hideFormProgressBar();
+ }
+ showVisitProgressBarAfterDelay() {
+ this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
+ }
+ hideVisitProgressBar() {
+ this.progressBar.hide();
+ if (this.visitProgressBarTimeout != null) {
+ window.clearTimeout(this.visitProgressBarTimeout);
+ delete this.visitProgressBarTimeout;
+ }
+ }
+ showFormProgressBarAfterDelay() {
+ if (this.formProgressBarTimeout == null) {
+ this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
+ }
+ }
+ hideFormProgressBar() {
+ this.progressBar.hide();
+ if (this.formProgressBarTimeout != null) {
+ window.clearTimeout(this.formProgressBarTimeout);
+ delete this.formProgressBarTimeout;
+ }
+ }
+ reload() {
+ window.location.reload();
+ }
+ get navigator() {
+ return this.session.navigator;
+ }
+ }
+
+ class CacheObserver {
+ constructor() {
+ this.started = false;
+ }
+ start() {
+ if (!this.started) {
+ this.started = true;
+ addEventListener("turbo:before-cache", this.removeStaleElements, false);
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.started = false;
+ removeEventListener("turbo:before-cache", this.removeStaleElements, false);
+ }
+ }
+ removeStaleElements() {
+ const staleElements = [...document.querySelectorAll('[data-turbo-cache="false"]')];
+ for (const element of staleElements) {
+ element.remove();
+ }
+ }
+ }
+
+ class FormSubmitObserver {
+ constructor(delegate) {
+ this.started = false;
+ this.submitCaptured = () => {
+ removeEventListener("submit", this.submitBubbled, false);
+ addEventListener("submit", this.submitBubbled, false);
+ };
+ this.submitBubbled = ((event) => {
+ if (!event.defaultPrevented) {
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
+ const submitter = event.submitter || undefined;
+ if (form) {
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
+ if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
+ event.preventDefault();
+ this.delegate.formSubmitted(form, submitter);
+ }
+ }
+ }
+ });
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ addEventListener("submit", this.submitCaptured, true);
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ removeEventListener("submit", this.submitCaptured, true);
+ this.started = false;
+ }
+ }
+ }
+
+ class FrameRedirector {
+ constructor(element) {
+ this.element = element;
+ this.linkInterceptor = new LinkInterceptor(this, element);
+ this.formInterceptor = new FormInterceptor(this, element);
+ }
+ start() {
+ this.linkInterceptor.start();
+ this.formInterceptor.start();
+ }
+ stop() {
+ this.linkInterceptor.stop();
+ this.formInterceptor.stop();
+ }
+ shouldInterceptLinkClick(element, url) {
+ return this.shouldRedirect(element);
+ }
+ linkClickIntercepted(element, url) {
+ const frame = this.findFrameElement(element);
+ if (frame) {
+ frame.delegate.linkClickIntercepted(element, url);
+ }
+ }
+ shouldInterceptFormSubmission(element, submitter) {
+ return this.shouldSubmit(element, submitter);
+ }
+ formSubmissionIntercepted(element, submitter) {
+ const frame = this.findFrameElement(element, submitter);
+ if (frame) {
+ frame.removeAttribute("reloadable");
+ frame.delegate.formSubmissionIntercepted(element, submitter);
+ }
+ }
+ shouldSubmit(form, submitter) {
+ var _a;
+ const action = getAction(form, submitter);
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
+ }
+ shouldRedirect(element, submitter) {
+ const frame = this.findFrameElement(element, submitter);
+ return frame ? frame != element.closest("turbo-frame") : false;
+ }
+ findFrameElement(element, submitter) {
+ const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame");
+ if (id && id != "_top") {
+ const frame = this.element.querySelector(`#${id}:not([disabled])`);
+ if (frame instanceof FrameElement) {
+ return frame;
+ }
+ }
+ }
+ }
+
+ class History {
+ constructor(delegate) {
+ this.restorationIdentifier = uuid();
+ this.restorationData = {};
+ this.started = false;
+ this.pageLoaded = false;
+ this.onPopState = (event) => {
+ if (this.shouldHandlePopState()) {
+ const { turbo } = event.state || {};
+ if (turbo) {
+ this.location = new URL(window.location.href);
+ const { restorationIdentifier } = turbo;
+ this.restorationIdentifier = restorationIdentifier;
+ this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
+ }
+ }
+ };
+ this.onPageLoad = async (event) => {
+ await nextMicrotask();
+ this.pageLoaded = true;
+ };
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ addEventListener("popstate", this.onPopState, false);
+ addEventListener("load", this.onPageLoad, false);
+ this.started = true;
+ this.replace(new URL(window.location.href));
+ }
+ }
+ stop() {
+ if (this.started) {
+ removeEventListener("popstate", this.onPopState, false);
+ removeEventListener("load", this.onPageLoad, false);
+ this.started = false;
+ }
+ }
+ push(location, restorationIdentifier) {
+ this.update(history.pushState, location, restorationIdentifier);
+ }
+ replace(location, restorationIdentifier) {
+ this.update(history.replaceState, location, restorationIdentifier);
+ }
+ update(method, location, restorationIdentifier = uuid()) {
+ const state = { turbo: { restorationIdentifier } };
+ method.call(history, state, "", location.href);
+ this.location = location;
+ this.restorationIdentifier = restorationIdentifier;
+ }
+ getRestorationDataForIdentifier(restorationIdentifier) {
+ return this.restorationData[restorationIdentifier] || {};
+ }
+ updateRestorationData(additionalData) {
+ const { restorationIdentifier } = this;
+ const restorationData = this.restorationData[restorationIdentifier];
+ this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData);
+ }
+ assumeControlOfScrollRestoration() {
+ var _a;
+ if (!this.previousScrollRestoration) {
+ this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : "auto";
+ history.scrollRestoration = "manual";
+ }
+ }
+ relinquishControlOfScrollRestoration() {
+ if (this.previousScrollRestoration) {
+ history.scrollRestoration = this.previousScrollRestoration;
+ delete this.previousScrollRestoration;
+ }
+ }
+ shouldHandlePopState() {
+ return this.pageIsLoaded();
+ }
+ pageIsLoaded() {
+ return this.pageLoaded || document.readyState == "complete";
+ }
+ }
+
+ class LinkClickObserver {
+ constructor(delegate) {
+ this.started = false;
+ this.clickCaptured = () => {
+ removeEventListener("click", this.clickBubbled, false);
+ addEventListener("click", this.clickBubbled, false);
+ };
+ this.clickBubbled = (event) => {
+ if (this.clickEventIsSignificant(event)) {
+ const target = (event.composedPath && event.composedPath()[0]) || event.target;
+ const link = this.findLinkFromClickTarget(target);
+ if (link) {
+ const location = this.getLocationForLink(link);
+ if (this.delegate.willFollowLinkToLocation(link, location)) {
+ event.preventDefault();
+ this.delegate.followedLinkToLocation(link, location);
+ }
+ }
+ }
+ };
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ addEventListener("click", this.clickCaptured, true);
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ removeEventListener("click", this.clickCaptured, true);
+ this.started = false;
+ }
+ }
+ clickEventIsSignificant(event) {
+ return !((event.target && event.target.isContentEditable)
+ || event.defaultPrevented
+ || event.which > 1
+ || event.altKey
+ || event.ctrlKey
+ || event.metaKey
+ || event.shiftKey);
+ }
+ findLinkFromClickTarget(target) {
+ if (target instanceof Element) {
+ return target.closest("a[href]:not([target^=_]):not([download])");
+ }
+ }
+ getLocationForLink(link) {
+ return expandURL(link.getAttribute("href") || "");
+ }
+ }
+
+ function isAction(action) {
+ return action == "advance" || action == "replace" || action == "restore";
+ }
+
+ class Navigator {
+ constructor(delegate) {
+ this.delegate = delegate;
+ }
+ proposeVisit(location, options = {}) {
+ if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
+ this.delegate.visitProposedToLocation(location, options);
+ }
+ else {
+ window.location.href = location.toString();
+ }
+ }
+ }
+ startVisit(locatable, restorationIdentifier, options = {}) {
+ this.stop();
+ this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
+ this.currentVisit.start();
+ }
+ submitForm(form, submitter) {
+ this.stop();
+ this.formSubmission = new FormSubmission(this, form, submitter, true);
+ this.formSubmission.start();
+ }
+ stop() {
+ if (this.formSubmission) {
+ this.formSubmission.stop();
+ delete this.formSubmission;
+ }
+ if (this.currentVisit) {
+ this.currentVisit.cancel();
+ delete this.currentVisit;
+ }
+ }
+ get adapter() {
+ return this.delegate.adapter;
+ }
+ get view() {
+ return this.delegate.view;
+ }
+ get history() {
+ return this.delegate.history;
+ }
+ formSubmissionStarted(formSubmission) {
+ if (typeof this.adapter.formSubmissionStarted === 'function') {
+ this.adapter.formSubmissionStarted(formSubmission);
+ }
+ }
+ async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
+ if (formSubmission == this.formSubmission) {
+ const responseHTML = await fetchResponse.responseHTML;
+ if (responseHTML) {
+ if (formSubmission.method != FetchMethod.get) {
+ this.view.clearSnapshotCache();
+ }
+ const { statusCode, redirected } = fetchResponse;
+ const action = this.getActionForFormSubmission(formSubmission);
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
+ this.proposeVisit(fetchResponse.location, visitOptions);
+ }
+ }
+ }
+ async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
+ const responseHTML = await fetchResponse.responseHTML;
+ if (responseHTML) {
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
+ if (fetchResponse.serverError) {
+ await this.view.renderError(snapshot);
+ }
+ else {
+ await this.view.renderPage(snapshot);
+ }
+ this.view.scrollToTop();
+ this.view.clearSnapshotCache();
+ }
+ }
+ formSubmissionErrored(formSubmission, error) {
+ console.error(error);
+ }
+ formSubmissionFinished(formSubmission) {
+ if (typeof this.adapter.formSubmissionFinished === 'function') {
+ this.adapter.formSubmissionFinished(formSubmission);
+ }
+ }
+ visitStarted(visit) {
+ this.delegate.visitStarted(visit);
+ }
+ visitCompleted(visit) {
+ this.delegate.visitCompleted(visit);
+ }
+ locationWithActionIsSamePage(location, action) {
+ const anchor = getAnchor(location);
+ const currentAnchor = getAnchor(this.view.lastRenderedLocation);
+ const isRestorationToTop = action === 'restore' && typeof anchor === 'undefined';
+ return action !== "replace" &&
+ getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
+ (isRestorationToTop || (anchor != null && anchor !== currentAnchor));
+ }
+ visitScrolledToSamePageLocation(oldURL, newURL) {
+ this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
+ }
+ get location() {
+ return this.history.location;
+ }
+ get restorationIdentifier() {
+ return this.history.restorationIdentifier;
+ }
+ getActionForFormSubmission(formSubmission) {
+ const { formElement, submitter } = formSubmission;
+ const action = getAttribute("data-turbo-action", submitter, formElement);
+ return isAction(action) ? action : "advance";
+ }
+ }
+
+ var PageStage;
+ (function (PageStage) {
+ PageStage[PageStage["initial"] = 0] = "initial";
+ PageStage[PageStage["loading"] = 1] = "loading";
+ PageStage[PageStage["interactive"] = 2] = "interactive";
+ PageStage[PageStage["complete"] = 3] = "complete";
+ })(PageStage || (PageStage = {}));
+ class PageObserver {
+ constructor(delegate) {
+ this.stage = PageStage.initial;
+ this.started = false;
+ this.interpretReadyState = () => {
+ const { readyState } = this;
+ if (readyState == "interactive") {
+ this.pageIsInteractive();
+ }
+ else if (readyState == "complete") {
+ this.pageIsComplete();
+ }
+ };
+ this.pageWillUnload = () => {
+ this.delegate.pageWillUnload();
+ };
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ if (this.stage == PageStage.initial) {
+ this.stage = PageStage.loading;
+ }
+ document.addEventListener("readystatechange", this.interpretReadyState, false);
+ addEventListener("pagehide", this.pageWillUnload, false);
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ document.removeEventListener("readystatechange", this.interpretReadyState, false);
+ removeEventListener("pagehide", this.pageWillUnload, false);
+ this.started = false;
+ }
+ }
+ pageIsInteractive() {
+ if (this.stage == PageStage.loading) {
+ this.stage = PageStage.interactive;
+ this.delegate.pageBecameInteractive();
+ }
+ }
+ pageIsComplete() {
+ this.pageIsInteractive();
+ if (this.stage == PageStage.interactive) {
+ this.stage = PageStage.complete;
+ this.delegate.pageLoaded();
+ }
+ }
+ get readyState() {
+ return document.readyState;
+ }
+ }
+
+ class ScrollObserver {
+ constructor(delegate) {
+ this.started = false;
+ this.onScroll = () => {
+ this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset });
+ };
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ addEventListener("scroll", this.onScroll, false);
+ this.onScroll();
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ removeEventListener("scroll", this.onScroll, false);
+ this.started = false;
+ }
+ }
+ updatePosition(position) {
+ this.delegate.scrollPositionChanged(position);
+ }
+ }
+
+ class StreamObserver {
+ constructor(delegate) {
+ this.sources = new Set;
+ this.started = false;
+ this.inspectFetchResponse = ((event) => {
+ const response = fetchResponseFromEvent(event);
+ if (response && fetchResponseIsStream(response)) {
+ event.preventDefault();
+ this.receiveMessageResponse(response);
+ }
+ });
+ this.receiveMessageEvent = (event) => {
+ if (this.started && typeof event.data == "string") {
+ this.receiveMessageHTML(event.data);
+ }
+ };
+ this.delegate = delegate;
+ }
+ start() {
+ if (!this.started) {
+ this.started = true;
+ addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.started = false;
+ removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
+ }
+ }
+ connectStreamSource(source) {
+ if (!this.streamSourceIsConnected(source)) {
+ this.sources.add(source);
+ source.addEventListener("message", this.receiveMessageEvent, false);
+ }
+ }
+ disconnectStreamSource(source) {
+ if (this.streamSourceIsConnected(source)) {
+ this.sources.delete(source);
+ source.removeEventListener("message", this.receiveMessageEvent, false);
+ }
+ }
+ streamSourceIsConnected(source) {
+ return this.sources.has(source);
+ }
+ async receiveMessageResponse(response) {
+ const html = await response.responseHTML;
+ if (html) {
+ this.receiveMessageHTML(html);
+ }
+ }
+ receiveMessageHTML(html) {
+ this.delegate.receivedMessageFromStream(new StreamMessage(html));
+ }
+ }
+ function fetchResponseFromEvent(event) {
+ var _a;
+ const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
+ if (fetchResponse instanceof FetchResponse) {
+ return fetchResponse;
+ }
+ }
+ function fetchResponseIsStream(response) {
+ var _a;
+ const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
+ return contentType.startsWith(StreamMessage.contentType);
+ }
+
+ class ErrorRenderer extends Renderer {
+ async render() {
+ this.replaceHeadAndBody();
+ this.activateScriptElements();
+ }
+ replaceHeadAndBody() {
+ const { documentElement, head, body } = document;
+ documentElement.replaceChild(this.newHead, head);
+ documentElement.replaceChild(this.newElement, body);
+ }
+ activateScriptElements() {
+ for (const replaceableElement of this.scriptElements) {
+ const parentNode = replaceableElement.parentNode;
+ if (parentNode) {
+ const element = this.createScriptElement(replaceableElement);
+ parentNode.replaceChild(element, replaceableElement);
+ }
+ }
+ }
+ get newHead() {
+ return this.newSnapshot.headSnapshot.element;
+ }
+ get scriptElements() {
+ return [...document.documentElement.querySelectorAll("script")];
+ }
+ }
+
+ class PageRenderer extends Renderer {
+ get shouldRender() {
+ return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
+ }
+ prepareToRender() {
+ this.mergeHead();
+ }
+ async render() {
+ if (this.willRender) {
+ this.replaceBody();
+ }
+ }
+ finishRendering() {
+ super.finishRendering();
+ if (!this.isPreview) {
+ this.focusFirstAutofocusableElement();
+ }
+ }
+ get currentHeadSnapshot() {
+ return this.currentSnapshot.headSnapshot;
+ }
+ get newHeadSnapshot() {
+ return this.newSnapshot.headSnapshot;
+ }
+ get newElement() {
+ return this.newSnapshot.element;
+ }
+ mergeHead() {
+ this.copyNewHeadStylesheetElements();
+ this.copyNewHeadScriptElements();
+ this.removeCurrentHeadProvisionalElements();
+ this.copyNewHeadProvisionalElements();
+ }
+ replaceBody() {
+ this.preservingPermanentElements(() => {
+ this.activateNewBody();
+ this.assignNewBody();
+ });
+ }
+ get trackedElementsAreIdentical() {
+ return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
+ }
+ copyNewHeadStylesheetElements() {
+ for (const element of this.newHeadStylesheetElements) {
+ document.head.appendChild(element);
+ }
+ }
+ copyNewHeadScriptElements() {
+ for (const element of this.newHeadScriptElements) {
+ document.head.appendChild(this.createScriptElement(element));
+ }
+ }
+ removeCurrentHeadProvisionalElements() {
+ for (const element of this.currentHeadProvisionalElements) {
+ document.head.removeChild(element);
+ }
+ }
+ copyNewHeadProvisionalElements() {
+ for (const element of this.newHeadProvisionalElements) {
+ document.head.appendChild(element);
+ }
+ }
+ activateNewBody() {
+ document.adoptNode(this.newElement);
+ this.activateNewBodyScriptElements();
+ }
+ activateNewBodyScriptElements() {
+ for (const inertScriptElement of this.newBodyScriptElements) {
+ const activatedScriptElement = this.createScriptElement(inertScriptElement);
+ inertScriptElement.replaceWith(activatedScriptElement);
+ }
+ }
+ assignNewBody() {
+ if (document.body && this.newElement instanceof HTMLBodyElement) {
+ document.body.replaceWith(this.newElement);
+ }
+ else {
+ document.documentElement.appendChild(this.newElement);
+ }
+ }
+ get newHeadStylesheetElements() {
+ return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
+ }
+ get newHeadScriptElements() {
+ return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
+ }
+ get currentHeadProvisionalElements() {
+ return this.currentHeadSnapshot.provisionalElements;
+ }
+ get newHeadProvisionalElements() {
+ return this.newHeadSnapshot.provisionalElements;
+ }
+ get newBodyScriptElements() {
+ return this.newElement.querySelectorAll("script");
+ }
+ }
+
+ class SnapshotCache {
+ constructor(size) {
+ this.keys = [];
+ this.snapshots = {};
+ this.size = size;
+ }
+ has(location) {
+ return toCacheKey(location) in this.snapshots;
+ }
+ get(location) {
+ if (this.has(location)) {
+ const snapshot = this.read(location);
+ this.touch(location);
+ return snapshot;
+ }
+ }
+ put(location, snapshot) {
+ this.write(location, snapshot);
+ this.touch(location);
+ return snapshot;
+ }
+ clear() {
+ this.snapshots = {};
+ }
+ read(location) {
+ return this.snapshots[toCacheKey(location)];
+ }
+ write(location, snapshot) {
+ this.snapshots[toCacheKey(location)] = snapshot;
+ }
+ touch(location) {
+ const key = toCacheKey(location);
+ const index = this.keys.indexOf(key);
+ if (index > -1)
+ this.keys.splice(index, 1);
+ this.keys.unshift(key);
+ this.trim();
+ }
+ trim() {
+ for (const key of this.keys.splice(this.size)) {
+ delete this.snapshots[key];
+ }
+ }
+ }
+
+ class PageView extends View {
+ constructor() {
+ super(...arguments);
+ this.snapshotCache = new SnapshotCache(10);
+ this.lastRenderedLocation = new URL(location.href);
+ }
+ renderPage(snapshot, isPreview = false, willRender = true) {
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
+ return this.render(renderer);
+ }
+ renderError(snapshot) {
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
+ return this.render(renderer);
+ }
+ clearSnapshotCache() {
+ this.snapshotCache.clear();
+ }
+ async cacheSnapshot() {
+ if (this.shouldCacheSnapshot) {
+ this.delegate.viewWillCacheSnapshot();
+ const { snapshot, lastRenderedLocation: location } = this;
+ await nextEventLoopTick();
+ const cachedSnapshot = snapshot.clone();
+ this.snapshotCache.put(location, cachedSnapshot);
+ return cachedSnapshot;
+ }
+ }
+ getCachedSnapshotForLocation(location) {
+ return this.snapshotCache.get(location);
+ }
+ get snapshot() {
+ return PageSnapshot.fromElement(this.element);
+ }
+ get shouldCacheSnapshot() {
+ return this.snapshot.isCacheable;
+ }
+ }
+
+ class Session {
+ constructor() {
+ this.navigator = new Navigator(this);
+ this.history = new History(this);
+ this.view = new PageView(this, document.documentElement);
+ this.adapter = new BrowserAdapter(this);
+ this.pageObserver = new PageObserver(this);
+ this.cacheObserver = new CacheObserver();
+ this.linkClickObserver = new LinkClickObserver(this);
+ this.formSubmitObserver = new FormSubmitObserver(this);
+ this.scrollObserver = new ScrollObserver(this);
+ this.streamObserver = new StreamObserver(this);
+ this.frameRedirector = new FrameRedirector(document.documentElement);
+ this.drive = true;
+ this.enabled = true;
+ this.progressBarDelay = 500;
+ this.started = false;
+ }
+ start() {
+ if (!this.started) {
+ this.pageObserver.start();
+ this.cacheObserver.start();
+ this.linkClickObserver.start();
+ this.formSubmitObserver.start();
+ this.scrollObserver.start();
+ this.streamObserver.start();
+ this.frameRedirector.start();
+ this.history.start();
+ this.started = true;
+ this.enabled = true;
+ }
+ }
+ disable() {
+ this.enabled = false;
+ }
+ stop() {
+ if (this.started) {
+ this.pageObserver.stop();
+ this.cacheObserver.stop();
+ this.linkClickObserver.stop();
+ this.formSubmitObserver.stop();
+ this.scrollObserver.stop();
+ this.streamObserver.stop();
+ this.frameRedirector.stop();
+ this.history.stop();
+ this.started = false;
+ }
+ }
+ registerAdapter(adapter) {
+ this.adapter = adapter;
+ }
+ visit(location, options = {}) {
+ this.navigator.proposeVisit(expandURL(location), options);
+ }
+ connectStreamSource(source) {
+ this.streamObserver.connectStreamSource(source);
+ }
+ disconnectStreamSource(source) {
+ this.streamObserver.disconnectStreamSource(source);
+ }
+ renderStreamMessage(message) {
+ document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
+ }
+ clearCache() {
+ this.view.clearSnapshotCache();
+ }
+ setProgressBarDelay(delay) {
+ this.progressBarDelay = delay;
+ }
+ get location() {
+ return this.history.location;
+ }
+ get restorationIdentifier() {
+ return this.history.restorationIdentifier;
+ }
+ historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
+ if (this.enabled) {
+ this.navigator.startVisit(location, restorationIdentifier, { action: "restore", historyChanged: true });
+ }
+ else {
+ this.adapter.pageInvalidated();
+ }
+ }
+ scrollPositionChanged(position) {
+ this.history.updateRestorationData({ scrollPosition: position });
+ }
+ willFollowLinkToLocation(link, location) {
+ return this.elementDriveEnabled(link)
+ && locationIsVisitable(location, this.snapshot.rootLocation)
+ && this.applicationAllowsFollowingLinkToLocation(link, location);
+ }
+ followedLinkToLocation(link, location) {
+ const action = this.getActionForLink(link);
+ this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
+ }
+ convertLinkWithMethodClickToFormSubmission(link) {
+ const linkMethod = link.getAttribute("data-turbo-method");
+ if (linkMethod) {
+ const form = document.createElement("form");
+ form.method = linkMethod;
+ form.action = link.getAttribute("href") || "undefined";
+ form.hidden = true;
+ if (link.hasAttribute("data-turbo-confirm")) {
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
+ }
+ const frame = this.getTargetFrameForLink(link);
+ if (frame) {
+ form.setAttribute("data-turbo-frame", frame);
+ form.addEventListener("turbo:submit-start", () => form.remove());
+ }
+ else {
+ form.addEventListener("submit", () => form.remove());
+ }
+ document.body.appendChild(form);
+ return dispatch("submit", { cancelable: true, target: form });
+ }
+ else {
+ return false;
+ }
+ }
+ allowsVisitingLocationWithAction(location, action) {
+ return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
+ }
+ visitProposedToLocation(location, options) {
+ extendURLWithDeprecatedProperties(location);
+ this.adapter.visitProposedToLocation(location, options);
+ }
+ visitStarted(visit) {
+ extendURLWithDeprecatedProperties(visit.location);
+ if (!visit.silent) {
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
+ }
+ }
+ visitCompleted(visit) {
+ this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
+ }
+ locationWithActionIsSamePage(location, action) {
+ return this.navigator.locationWithActionIsSamePage(location, action);
+ }
+ visitScrolledToSamePageLocation(oldURL, newURL) {
+ this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
+ }
+ willSubmitForm(form, submitter) {
+ const action = getAction(form, submitter);
+ return this.elementDriveEnabled(form)
+ && (!submitter || this.elementDriveEnabled(submitter))
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
+ }
+ formSubmitted(form, submitter) {
+ this.navigator.submitForm(form, submitter);
+ }
+ pageBecameInteractive() {
+ this.view.lastRenderedLocation = this.location;
+ this.notifyApplicationAfterPageLoad();
+ }
+ pageLoaded() {
+ this.history.assumeControlOfScrollRestoration();
+ }
+ pageWillUnload() {
+ this.history.relinquishControlOfScrollRestoration();
+ }
+ receivedMessageFromStream(message) {
+ this.renderStreamMessage(message);
+ }
+ viewWillCacheSnapshot() {
+ var _a;
+ if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
+ this.notifyApplicationBeforeCachingSnapshot();
+ }
+ }
+ allowsImmediateRender({ element }, resume) {
+ const event = this.notifyApplicationBeforeRender(element, resume);
+ return !event.defaultPrevented;
+ }
+ viewRenderedSnapshot(snapshot, isPreview) {
+ this.view.lastRenderedLocation = this.history.location;
+ this.notifyApplicationAfterRender();
+ }
+ viewInvalidated() {
+ this.adapter.pageInvalidated();
+ }
+ frameLoaded(frame) {
+ this.notifyApplicationAfterFrameLoad(frame);
+ }
+ frameRendered(fetchResponse, frame) {
+ this.notifyApplicationAfterFrameRender(fetchResponse, frame);
+ }
+ applicationAllowsFollowingLinkToLocation(link, location) {
+ const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
+ return !event.defaultPrevented;
+ }
+ applicationAllowsVisitingLocation(location) {
+ const event = this.notifyApplicationBeforeVisitingLocation(location);
+ return !event.defaultPrevented;
+ }
+ notifyApplicationAfterClickingLinkToLocation(link, location) {
+ return dispatch("turbo:click", { target: link, detail: { url: location.href }, cancelable: true });
+ }
+ notifyApplicationBeforeVisitingLocation(location) {
+ return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
+ }
+ notifyApplicationAfterVisitingLocation(location, action) {
+ markAsBusy(document.documentElement);
+ return dispatch("turbo:visit", { detail: { url: location.href, action } });
+ }
+ notifyApplicationBeforeCachingSnapshot() {
+ return dispatch("turbo:before-cache");
+ }
+ notifyApplicationBeforeRender(newBody, resume) {
+ return dispatch("turbo:before-render", { detail: { newBody, resume }, cancelable: true });
+ }
+ notifyApplicationAfterRender() {
+ return dispatch("turbo:render");
+ }
+ notifyApplicationAfterPageLoad(timing = {}) {
+ clearBusyState(document.documentElement);
+ return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
+ }
+ notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
+ dispatchEvent(new HashChangeEvent("hashchange", { oldURL: oldURL.toString(), newURL: newURL.toString() }));
+ }
+ notifyApplicationAfterFrameLoad(frame) {
+ return dispatch("turbo:frame-load", { target: frame });
+ }
+ notifyApplicationAfterFrameRender(fetchResponse, frame) {
+ return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, cancelable: true });
+ }
+ elementDriveEnabled(element) {
+ const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
+ if (this.drive) {
+ if (container) {
+ return container.getAttribute("data-turbo") != "false";
+ }
+ else {
+ return true;
+ }
+ }
+ else {
+ if (container) {
+ return container.getAttribute("data-turbo") == "true";
+ }
+ else {
+ return false;
+ }
+ }
+ }
+ getActionForLink(link) {
+ const action = link.getAttribute("data-turbo-action");
+ return isAction(action) ? action : "advance";
+ }
+ getTargetFrameForLink(link) {
+ const frame = link.getAttribute("data-turbo-frame");
+ if (frame) {
+ return frame;
+ }
+ else {
+ const container = link.closest("turbo-frame");
+ if (container) {
+ return container.id;
+ }
+ }
+ }
+ get snapshot() {
+ return this.view.snapshot;
+ }
+ }
+ function extendURLWithDeprecatedProperties(url) {
+ Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
+ }
+ const deprecatedLocationPropertyDescriptors = {
+ absoluteURL: {
+ get() {
+ return this.toString();
+ }
+ }
+ };
+
+ const session = new Session;
+ const { navigator: navigator$1 } = session;
+ function start() {
+ session.start();
+ }
+ function registerAdapter(adapter) {
+ session.registerAdapter(adapter);
+ }
+ function visit(location, options) {
+ session.visit(location, options);
+ }
+ function connectStreamSource(source) {
+ session.connectStreamSource(source);
+ }
+ function disconnectStreamSource(source) {
+ session.disconnectStreamSource(source);
+ }
+ function renderStreamMessage(message) {
+ session.renderStreamMessage(message);
+ }
+ function clearCache() {
+ session.clearCache();
+ }
+ function setProgressBarDelay(delay) {
+ session.setProgressBarDelay(delay);
+ }
+ function setConfirmMethod(confirmMethod) {
+ FormSubmission.confirmMethod = confirmMethod;
+ }
+
+ var Turbo = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ navigator: navigator$1,
+ session: session,
+ PageRenderer: PageRenderer,
+ PageSnapshot: PageSnapshot,
+ start: start,
+ registerAdapter: registerAdapter,
+ visit: visit,
+ connectStreamSource: connectStreamSource,
+ disconnectStreamSource: disconnectStreamSource,
+ renderStreamMessage: renderStreamMessage,
+ clearCache: clearCache,
+ setProgressBarDelay: setProgressBarDelay,
+ setConfirmMethod: setConfirmMethod
+ });
+
+ class FrameController {
+ constructor(element) {
+ this.fetchResponseLoaded = (fetchResponse) => { };
+ this.currentFetchRequest = null;
+ this.resolveVisitPromise = () => { };
+ this.connected = false;
+ this.hasBeenLoaded = false;
+ this.settingSourceURL = false;
+ this.element = element;
+ this.view = new FrameView(this, this.element);
+ this.appearanceObserver = new AppearanceObserver(this, this.element);
+ this.linkInterceptor = new LinkInterceptor(this, this.element);
+ this.formInterceptor = new FormInterceptor(this, this.element);
+ }
+ connect() {
+ if (!this.connected) {
+ this.connected = true;
+ this.reloadable = false;
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
+ this.appearanceObserver.start();
+ }
+ this.linkInterceptor.start();
+ this.formInterceptor.start();
+ this.sourceURLChanged();
+ }
+ }
+ disconnect() {
+ if (this.connected) {
+ this.connected = false;
+ this.appearanceObserver.stop();
+ this.linkInterceptor.stop();
+ this.formInterceptor.stop();
+ }
+ }
+ disabledChanged() {
+ if (this.loadingStyle == FrameLoadingStyle.eager) {
+ this.loadSourceURL();
+ }
+ }
+ sourceURLChanged() {
+ if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
+ this.loadSourceURL();
+ }
+ }
+ loadingStyleChanged() {
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
+ this.appearanceObserver.start();
+ }
+ else {
+ this.appearanceObserver.stop();
+ this.loadSourceURL();
+ }
+ }
+ async loadSourceURL() {
+ if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
+ const previousURL = this.currentURL;
+ this.currentURL = this.sourceURL;
+ if (this.sourceURL) {
+ try {
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
+ this.appearanceObserver.stop();
+ await this.element.loaded;
+ this.hasBeenLoaded = true;
+ }
+ catch (error) {
+ this.currentURL = previousURL;
+ throw error;
+ }
+ }
+ }
+ }
+ async loadResponse(fetchResponse) {
+ if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
+ this.sourceURL = fetchResponse.response.url;
+ }
+ try {
+ const html = await fetchResponse.responseHTML;
+ if (html) {
+ const { body } = parseHTMLDocument(html);
+ const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
+ if (this.view.renderPromise)
+ await this.view.renderPromise;
+ await this.view.render(renderer);
+ session.frameRendered(fetchResponse, this.element);
+ session.frameLoaded(this.element);
+ this.fetchResponseLoaded(fetchResponse);
+ }
+ }
+ catch (error) {
+ console.error(error);
+ this.view.invalidate();
+ }
+ finally {
+ this.fetchResponseLoaded = () => { };
+ }
+ }
+ elementAppearedInViewport(element) {
+ this.loadSourceURL();
+ }
+ shouldInterceptLinkClick(element, url) {
+ if (element.hasAttribute("data-turbo-method")) {
+ return false;
+ }
+ else {
+ return this.shouldInterceptNavigation(element);
+ }
+ }
+ linkClickIntercepted(element, url) {
+ this.reloadable = true;
+ this.navigateFrame(element, url);
+ }
+ shouldInterceptFormSubmission(element, submitter) {
+ return this.shouldInterceptNavigation(element, submitter);
+ }
+ formSubmissionIntercepted(element, submitter) {
+ if (this.formSubmission) {
+ this.formSubmission.stop();
+ }
+ this.reloadable = false;
+ this.formSubmission = new FormSubmission(this, element, submitter);
+ const { fetchRequest } = this.formSubmission;
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
+ this.formSubmission.start();
+ }
+ prepareHeadersForRequest(headers, request) {
+ headers["Turbo-Frame"] = this.id;
+ }
+ requestStarted(request) {
+ markAsBusy(this.element);
+ }
+ requestPreventedHandlingResponse(request, response) {
+ this.resolveVisitPromise();
+ }
+ async requestSucceededWithResponse(request, response) {
+ await this.loadResponse(response);
+ this.resolveVisitPromise();
+ }
+ requestFailedWithResponse(request, response) {
+ console.error(response);
+ this.resolveVisitPromise();
+ }
+ requestErrored(request, error) {
+ console.error(error);
+ this.resolveVisitPromise();
+ }
+ requestFinished(request) {
+ clearBusyState(this.element);
+ }
+ formSubmissionStarted({ formElement }) {
+ markAsBusy(formElement, this.findFrameElement(formElement));
+ }
+ formSubmissionSucceededWithResponse(formSubmission, response) {
+ const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
+ frame.delegate.loadResponse(response);
+ }
+ formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
+ this.element.delegate.loadResponse(fetchResponse);
+ }
+ formSubmissionErrored(formSubmission, error) {
+ console.error(error);
+ }
+ formSubmissionFinished({ formElement }) {
+ clearBusyState(formElement, this.findFrameElement(formElement));
+ }
+ allowsImmediateRender(snapshot, resume) {
+ return true;
+ }
+ viewRenderedSnapshot(snapshot, isPreview) {
+ }
+ viewInvalidated() {
+ }
+ async visit(url) {
+ var _a;
+ const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
+ this.currentFetchRequest = request;
+ return new Promise(resolve => {
+ this.resolveVisitPromise = () => {
+ this.resolveVisitPromise = () => { };
+ this.currentFetchRequest = null;
+ resolve();
+ };
+ request.perform();
+ });
+ }
+ navigateFrame(element, url, submitter) {
+ const frame = this.findFrameElement(element, submitter);
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
+ frame.setAttribute("reloadable", "");
+ frame.src = url;
+ }
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
+ if (isAction(action)) {
+ const { visitCachedSnapshot } = new SnapshotSubstitution(frame);
+ frame.delegate.fetchResponseLoaded = (fetchResponse) => {
+ if (frame.src) {
+ const { statusCode, redirected } = fetchResponse;
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
+ const response = { statusCode, redirected, responseHTML };
+ session.visit(frame.src, { action, response, visitCachedSnapshot, willRender: false });
+ }
+ };
+ }
+ }
+ findFrameElement(element, submitter) {
+ var _a;
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
+ return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
+ }
+ async extractForeignFrameElement(container) {
+ let element;
+ const id = CSS.escape(this.id);
+ try {
+ if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
+ return element;
+ }
+ if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
+ await element.loaded;
+ return await this.extractForeignFrameElement(element);
+ }
+ console.error(`Response has no matching <turbo-frame id="${id}"> element`);
+ }
+ catch (error) {
+ console.error(error);
+ }
+ return new FrameElement();
+ }
+ formActionIsVisitable(form, submitter) {
+ const action = getAction(form, submitter);
+ return locationIsVisitable(expandURL(action), this.rootLocation);
+ }
+ shouldInterceptNavigation(element, submitter) {
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
+ return false;
+ }
+ if (!this.enabled || id == "_top") {
+ return false;
+ }
+ if (id) {
+ const frameElement = getFrameElementById(id);
+ if (frameElement) {
+ return !frameElement.disabled;
+ }
+ }
+ if (!session.elementDriveEnabled(element)) {
+ return false;
+ }
+ if (submitter && !session.elementDriveEnabled(submitter)) {
+ return false;
+ }
+ return true;
+ }
+ get id() {
+ return this.element.id;
+ }
+ get enabled() {
+ return !this.element.disabled;
+ }
+ get sourceURL() {
+ if (this.element.src) {
+ return this.element.src;
+ }
+ }
+ get reloadable() {
+ const frame = this.findFrameElement(this.element);
+ return frame.hasAttribute("reloadable");
+ }
+ set reloadable(value) {
+ const frame = this.findFrameElement(this.element);
+ if (value) {
+ frame.setAttribute("reloadable", "");
+ }
+ else {
+ frame.removeAttribute("reloadable");
+ }
+ }
+ set sourceURL(sourceURL) {
+ this.settingSourceURL = true;
+ this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
+ this.currentURL = this.element.src;
+ this.settingSourceURL = false;
+ }
+ get loadingStyle() {
+ return this.element.loading;
+ }
+ get isLoading() {
+ return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
+ }
+ get isActive() {
+ return this.element.isActive && this.connected;
+ }
+ get rootLocation() {
+ var _a;
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
+ return expandURL(root);
+ }
+ }
+ class SnapshotSubstitution {
+ constructor(element) {
+ this.visitCachedSnapshot = ({ element }) => {
+ var _a;
+ const { id, clone } = this;
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
+ };
+ this.clone = element.cloneNode(true);
+ this.id = element.id;
+ }
+ }
+ function getFrameElementById(id) {
+ if (id != null) {
+ const element = document.getElementById(id);
+ if (element instanceof FrameElement) {
+ return element;
+ }
+ }
+ }
+ function activateElement(element, currentURL) {
+ if (element) {
+ const src = element.getAttribute("src");
+ if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {
+ throw new Error(`Matching <turbo-frame id="${element.id}"> element has a source URL which references itself`);
+ }
+ if (element.ownerDocument !== document) {
+ element = document.importNode(element, true);
+ }
+ if (element instanceof FrameElement) {
+ element.connectedCallback();
+ element.disconnectedCallback();
+ return element;
+ }
+ }
+ }
+
+ const StreamActions = {
+ after() {
+ this.targetElements.forEach(e => { var _a; return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling); });
+ },
+ append() {
+ this.removeDuplicateTargetChildren();
+ this.targetElements.forEach(e => e.append(this.templateContent));
+ },
+ before() {
+ this.targetElements.forEach(e => { var _a; return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e); });
+ },
+ prepend() {
+ this.removeDuplicateTargetChildren();
+ this.targetElements.forEach(e => e.prepend(this.templateContent));
+ },
+ remove() {
+ this.targetElements.forEach(e => e.remove());
+ },
+ replace() {
+ this.targetElements.forEach(e => e.replaceWith(this.templateContent));
+ },
+ update() {
+ this.targetElements.forEach(e => {
+ e.innerHTML = "";
+ e.append(this.templateContent);
+ });
+ }
+ };
+
+ class StreamElement extends HTMLElement {
+ async connectedCallback() {
+ try {
+ await this.render();
+ }
+ catch (error) {
+ console.error(error);
+ }
+ finally {
+ this.disconnect();
+ }
+ }
+ async render() {
+ var _a;
+ return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : (this.renderPromise = (async () => {
+ if (this.dispatchEvent(this.beforeRenderEvent)) {
+ await nextAnimationFrame();
+ this.performAction();
+ }
+ })());
+ }
+ disconnect() {
+ try {
+ this.remove();
+ }
+ catch (_a) { }
+ }
+ removeDuplicateTargetChildren() {
+ this.duplicateChildren.forEach(c => c.remove());
+ }
+ get duplicateChildren() {
+ var _a;
+ const existingChildren = this.targetElements.flatMap(e => [...e.children]).filter(c => !!c.id);
+ const newChildrenIds = [...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children].filter(c => !!c.id).map(c => c.id);
+ return existingChildren.filter(c => newChildrenIds.includes(c.id));
+ }
+ get performAction() {
+ if (this.action) {
+ const actionFunction = StreamActions[this.action];
+ if (actionFunction) {
+ return actionFunction;
+ }
+ this.raise("unknown action");
+ }
+ this.raise("action attribute is missing");
+ }
+ get targetElements() {
+ if (this.target) {
+ return this.targetElementsById;
+ }
+ else if (this.targets) {
+ return this.targetElementsByQuery;
+ }
+ else {
+ this.raise("target or targets attribute is missing");
+ }
+ }
+ get templateContent() {
+ return this.templateElement.content.cloneNode(true);
+ }
+ get templateElement() {
+ if (this.firstElementChild instanceof HTMLTemplateElement) {
+ return this.firstElementChild;
+ }
+ this.raise("first child element must be a <template> element");
+ }
+ get action() {
+ return this.getAttribute("action");
+ }
+ get target() {
+ return this.getAttribute("target");
+ }
+ get targets() {
+ return this.getAttribute("targets");
+ }
+ raise(message) {
+ throw new Error(`${this.description}: ${message}`);
+ }
+ get description() {
+ var _a, _b;
+ return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
+ }
+ get beforeRenderEvent() {
+ return new CustomEvent("turbo:before-stream-render", { bubbles: true, cancelable: true });
+ }
+ get targetElementsById() {
+ var _a;
+ const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
+ if (element !== null) {
+ return [element];
+ }
+ else {
+ return [];
+ }
+ }
+ get targetElementsByQuery() {
+ var _a;
+ const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
+ if (elements.length !== 0) {
+ return Array.prototype.slice.call(elements);
+ }
+ else {
+ return [];
+ }
+ }
+ }
+
+ FrameElement.delegateConstructor = FrameController;
+ customElements.define("turbo-frame", FrameElement);
+ customElements.define("turbo-stream", StreamElement);
+
+ (() => {
+ let element = document.currentScript;
+ if (!element)
+ return;
+ if (element.hasAttribute("data-turbo-suppress-warning"))
+ return;
+ while (element = element.parentElement) {
+ if (element == document.body) {
+ return console.warn(unindent `
+ You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
+
+ Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
+
+ For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
+
+ ——
+ Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
+ `, element.outerHTML);
+ }
+ }
+ })();
+
+ window.Turbo = Turbo;
+ start();
+
+ exports.PageRenderer = PageRenderer;
+ exports.PageSnapshot = PageSnapshot;
+ exports.clearCache = clearCache;
+ exports.connectStreamSource = connectStreamSource;
+ exports.disconnectStreamSource = disconnectStreamSource;
+ exports.navigator = navigator$1;
+ exports.registerAdapter = registerAdapter;
+ exports.renderStreamMessage = renderStreamMessage;
+ exports.session = session;
+ exports.setConfirmMethod = setConfirmMethod;
+ exports.setProgressBarDelay = setProgressBarDelay;
+ exports.start = start;
+ exports.visit = visit;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
commit 22b70e836069ef492a63fc97baf8e890364492f3
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Thu Jul 28 11:39:25 2022 -0400
Initial checkin
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8c15e01
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+blib*
+Makefile
+Makefile.old
+pm_to_blib*
+*.tar.gz
+.lwpcookies
+cover_db
+pod2htm*.tmp
+/RT-Extension-Turbo*
+*.bak
+*.swp
+/MYMETA.*
+/t/tmp
+/xt/tmp
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..1ce6aae
--- /dev/null
+++ b/Changes
@@ -0,0 +1,4 @@
+Revision history for RT-Extension-Turbo
+
+0.01 [Release Date]
+ - Initial version
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..018d6e1
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,29 @@
+---
+abstract: 'RT-Extension-Turbo Extension'
+author:
+ - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
+build_requires:
+ ExtUtils::MakeMaker: 6.59
+configure_requires:
+ ExtUtils::MakeMaker: 6.59
+distribution_type: module
+dynamic_config: 1
+generated_by: 'Module::Install version 1.19'
+license: gpl_2
+meta-spec:
+ url: http://module-build.sourceforge.net/META-spec-v1.4.html
+ version: 1.4
+name: RT-Extension-Turbo
+no_index:
+ directory:
+ - inc
+ - static
+requires:
+ perl: 5.10.1
+resources:
+ license: http://opensource.org/licenses/gpl-license.php
+ repository: https://github.com/bestpractical/rt-extension-turbo
+version: '0.01'
+x_module_install_rtx_version: '0.43'
+x_requires_rt: 5.0.0
+x_rt_too_new: 6.0.0
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..45ca53d
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,12 @@
+use lib '.';
+use inc::Module::Install;
+
+RTx 'RT-Extension-Turbo';
+license 'gpl_2';
+repository 'https://github.com/bestpractical/rt-extension-turbo';
+
+requires_rt '5.0.0';
+rt_too_new '6.0.0';
+
+sign;
+WriteAll;
diff --git a/README b/README
new file mode 100644
index 0000000..78fbf99
--- /dev/null
+++ b/README
@@ -0,0 +1,40 @@
+NAME
+ RT-Extension-Turbo - Experimental extension using Turbo
+
+DESCRIPTION
+ An experimental extension using Turbo <https://turbo.hotwired.dev/> with
+ RT.
+
+RT VERSION
+ Works with RT 5.
+
+INSTALLATION
+ perl Makefile.PL
+ make
+ make install
+ May need root permissions
+
+ Edit your /opt/rt4/etc/RT_SiteConfig.pm
+ Add this line:
+
+ Plugin('RT::Extension::Turbo');
+
+ Clear your mason cache
+ rm -rf /opt/rt4/var/mason_data/obj
+
+ Restart your webserver
+
+AUTHOR
+ Best Practical Solutions, LLC <modules at bestpractical.com>
+
+ All bugs should be reported via email to
+ bug-RT-Extension-Turbo at rt.cpan.org
+ or via the web at
+ http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-Turbo
+LICENSE AND COPYRIGHT
+ This software is Copyright (c) 2022 by Best Practical Solutions, LLC
+
+ This is free software, licensed under:
+
+ The GNU General Public License, Version 2, June 1991
+
diff --git a/lib/RT/Extension/Turbo.pm b/lib/RT/Extension/Turbo.pm
new file mode 100644
index 0000000..5a7e693
--- /dev/null
+++ b/lib/RT/Extension/Turbo.pm
@@ -0,0 +1,70 @@
+use strict;
+use warnings;
+package RT::Extension::Turbo;
+
+our $VERSION = '0.01';
+
+=head1 NAME
+
+RT-Extension-Turbo - Experimental extension using Turbo
+
+=head1 DESCRIPTION
+
+An experimental extension using L<Turbo|https://turbo.hotwired.dev/> with RT.
+
+=head1 RT VERSION
+
+Works with RT 5.
+
+=head1 INSTALLATION
+
+=over
+
+=item C<perl Makefile.PL>
+
+=item C<make>
+
+=item C<make install>
+
+May need root permissions
+
+=item Edit your F</opt/rt4/etc/RT_SiteConfig.pm>
+
+Add this line:
+
+ Plugin('RT::Extension::Turbo');
+
+=item Clear your mason cache
+
+ rm -rf /opt/rt4/var/mason_data/obj
+
+=item Restart your webserver
+
+=back
+
+=head1 AUTHOR
+
+Best Practical Solutions, LLC E<lt>modules at bestpractical.comE<gt>
+
+=for html <p>All bugs should be reported via email to <a
+href="mailto:bug-RT-Extension-Turbo at rt.cpan.org">bug-RT-Extension-Turbo at rt.cpan.org</a>
+or via the web at <a
+href="http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-Turbo">rt.cpan.org</a>.</p>
+
+=for text
+ All bugs should be reported via email to
+ bug-RT-Extension-Turbo at rt.cpan.org
+ or via the web at
+ http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-Turbo
+
+=head1 LICENSE AND COPYRIGHT
+
+This software is Copyright (c) 2022 by Best Practical Solutions, LLC
+
+This is free software, licensed under:
+
+ The GNU General Public License, Version 2, June 1991
+
+=cut
+
+1;
commit 1ab2facb56a37c219564844e188372310613b21d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Thu Jul 28 11:38:13 2022 -0400
Module::Install files
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
new file mode 100644
index 0000000..7ba98c2
--- /dev/null
+++ b/inc/Module/Install.pm
@@ -0,0 +1,451 @@
+#line 1
+package Module::Install;
+
+# For any maintainers:
+# The load order for Module::Install is a bit magic.
+# It goes something like this...
+#
+# IF ( host has Module::Install installed, creating author mode ) {
+# 1. Makefile.PL calls "use inc::Module::Install"
+# 2. $INC{inc/Module/Install.pm} set to installed version of inc::Module::Install
+# 3. The installed version of inc::Module::Install loads
+# 4. inc::Module::Install calls "require Module::Install"
+# 5. The ./inc/ version of Module::Install loads
+# } ELSE {
+# 1. Makefile.PL calls "use inc::Module::Install"
+# 2. $INC{inc/Module/Install.pm} set to ./inc/ version of Module::Install
+# 3. The ./inc/ version of Module::Install loads
+# }
+
+use 5.006;
+use strict 'vars';
+use Cwd ();
+use File::Find ();
+use File::Path ();
+
+use vars qw{$VERSION $MAIN};
+BEGIN {
+ # All Module::Install core packages now require synchronised versions.
+ # This will be used to ensure we don't accidentally load old or
+ # different versions of modules.
+ # This is not enforced yet, but will be some time in the next few
+ # releases once we can make sure it won't clash with custom
+ # Module::Install extensions.
+ $VERSION = '1.19';
+
+ # Storage for the pseudo-singleton
+ $MAIN = undef;
+
+ *inc::Module::Install::VERSION = *VERSION;
+ @inc::Module::Install::ISA = __PACKAGE__;
+
+}
+
+sub import {
+ my $class = shift;
+ my $self = $class->new(@_);
+ my $who = $self->_caller;
+
+ #-------------------------------------------------------------
+ # all of the following checks should be included in import(),
+ # to allow "eval 'require Module::Install; 1' to test
+ # installation of Module::Install. (RT #51267)
+ #-------------------------------------------------------------
+
+ # Whether or not inc::Module::Install is actually loaded, the
+ # $INC{inc/Module/Install.pm} is what will still get set as long as
+ # the caller loaded module this in the documented manner.
+ # If not set, the caller may NOT have loaded the bundled version, and thus
+ # they may not have a MI version that works with the Makefile.PL. This would
+ # result in false errors or unexpected behaviour. And we don't want that.
+ my $file = join( '/', 'inc', split /::/, __PACKAGE__ ) . '.pm';
+ unless ( $INC{$file} ) { die <<"END_DIE" }
+
+Please invoke ${\__PACKAGE__} with:
+
+ use inc::${\__PACKAGE__};
+
+not:
+
+ use ${\__PACKAGE__};
+
+END_DIE
+
+ # This reportedly fixes a rare Win32 UTC file time issue, but
+ # as this is a non-cross-platform XS module not in the core,
+ # we shouldn't really depend on it. See RT #24194 for detail.
+ # (Also, this module only supports Perl 5.6 and above).
+ eval "use Win32::UTCFileTime" if $^O eq 'MSWin32' && $] >= 5.006;
+
+ # If the script that is loading Module::Install is from the future,
+ # then make will detect this and cause it to re-run over and over
+ # again. This is bad. Rather than taking action to touch it (which
+ # is unreliable on some platforms and requires write permissions)
+ # for now we should catch this and refuse to run.
+ if ( -f $0 ) {
+ my $s = (stat($0))[9];
+
+ # If the modification time is only slightly in the future,
+ # sleep briefly to remove the problem.
+ my $a = $s - time;
+ if ( $a > 0 and $a < 5 ) { sleep 5 }
+
+ # Too far in the future, throw an error.
+ my $t = time;
+ if ( $s > $t ) { die <<"END_DIE" }
+
+Your installer $0 has a modification time in the future ($s > $t).
+
+This is known to create infinite loops in make.
+
+Please correct this, then run $0 again.
+
+END_DIE
+ }
+
+
+ # Build.PL was formerly supported, but no longer is due to excessive
+ # difficulty in implementing every single feature twice.
+ if ( $0 =~ /Build.PL$/i ) { die <<"END_DIE" }
+
+Module::Install no longer supports Build.PL.
+
+It was impossible to maintain duel backends, and has been deprecated.
+
+Please remove all Build.PL files and only use the Makefile.PL installer.
+
+END_DIE
+
+ #-------------------------------------------------------------
+
+ # To save some more typing in Module::Install installers, every...
+ # use inc::Module::Install
+ # ...also acts as an implicit use strict.
+ $^H |= strict::bits(qw(refs subs vars));
+
+ #-------------------------------------------------------------
+
+ unless ( -f $self->{file} ) {
+ foreach my $key (keys %INC) {
+ delete $INC{$key} if $key =~ /Module\/Install/;
+ }
+
+ local $^W;
+ require "$self->{path}/$self->{dispatch}.pm";
+ File::Path::mkpath("$self->{prefix}/$self->{author}");
+ $self->{admin} = "$self->{name}::$self->{dispatch}"->new( _top => $self );
+ $self->{admin}->init;
+ @_ = ($class, _self => $self);
+ goto &{"$self->{name}::import"};
+ }
+
+ local $^W;
+ *{"${who}::AUTOLOAD"} = $self->autoload;
+ $self->preload;
+
+ # Unregister loader and worker packages so subdirs can use them again
+ delete $INC{'inc/Module/Install.pm'};
+ delete $INC{'Module/Install.pm'};
+
+ # Save to the singleton
+ $MAIN = $self;
+
+ return 1;
+}
+
+sub autoload {
+ my $self = shift;
+ my $who = $self->_caller;
+ my $cwd = Cwd::getcwd();
+ my $sym = "${who}::AUTOLOAD";
+ $sym->{$cwd} = sub {
+ my $pwd = Cwd::getcwd();
+ if ( my $code = $sym->{$pwd} ) {
+ # Delegate back to parent dirs
+ goto &$code unless $cwd eq $pwd;
+ }
+ unless ($$sym =~ s/([^:]+)$//) {
+ # XXX: it looks like we can't retrieve the missing function
+ # via $$sym (usually $main::AUTOLOAD) in this case.
+ # I'm still wondering if we should slurp Makefile.PL to
+ # get some context or not ...
+ my ($package, $file, $line) = caller;
+ die <<"EOT";
+Unknown function is found at $file line $line.
+Execution of $file aborted due to runtime errors.
+
+If you're a contributor to a project, you may need to install
+some Module::Install extensions from CPAN (or other repository).
+If you're a user of a module, please contact the author.
+EOT
+ }
+ my $method = $1;
+ if ( uc($method) eq $method ) {
+ # Do nothing
+ return;
+ } elsif ( $method =~ /^_/ and $self->can($method) ) {
+ # Dispatch to the root M:I class
+ return $self->$method(@_);
+ }
+
+ # Dispatch to the appropriate plugin
+ unshift @_, ( $self, $1 );
+ goto &{$self->can('call')};
+ };
+}
+
+sub preload {
+ my $self = shift;
+ unless ( $self->{extensions} ) {
+ $self->load_extensions(
+ "$self->{prefix}/$self->{path}", $self
+ );
+ }
+
+ my @exts = @{$self->{extensions}};
+ unless ( @exts ) {
+ @exts = $self->{admin}->load_all_extensions;
+ }
+
+ my %seen;
+ foreach my $obj ( @exts ) {
+ while (my ($method, $glob) = each %{ref($obj) . '::'}) {
+ next unless $obj->can($method);
+ next if $method =~ /^_/;
+ next if $method eq uc($method);
+ $seen{$method}++;
+ }
+ }
+
+ my $who = $self->_caller;
+ foreach my $name ( sort keys %seen ) {
+ local $^W;
+ *{"${who}::$name"} = sub {
+ ${"${who}::AUTOLOAD"} = "${who}::$name";
+ goto &{"${who}::AUTOLOAD"};
+ };
+ }
+}
+
+sub new {
+ my ($class, %args) = @_;
+
+ delete $INC{'FindBin.pm'};
+ {
+ # to suppress the redefine warning
+ local $SIG{__WARN__} = sub {};
+ require FindBin;
+ }
+
+ # ignore the prefix on extension modules built from top level.
+ my $base_path = Cwd::abs_path($FindBin::Bin);
+ unless ( Cwd::abs_path(Cwd::getcwd()) eq $base_path ) {
+ delete $args{prefix};
+ }
+ return $args{_self} if $args{_self};
+
+ $base_path = VMS::Filespec::unixify($base_path) if $^O eq 'VMS';
+
+ $args{dispatch} ||= 'Admin';
+ $args{prefix} ||= 'inc';
+ $args{author} ||= ($^O eq 'VMS' ? '_author' : '.author');
+ $args{bundle} ||= 'inc/BUNDLES';
+ $args{base} ||= $base_path;
+ $class =~ s/^\Q$args{prefix}\E:://;
+ $args{name} ||= $class;
+ $args{version} ||= $class->VERSION;
+ unless ( $args{path} ) {
+ $args{path} = $args{name};
+ $args{path} =~ s!::!/!g;
+ }
+ $args{file} ||= "$args{base}/$args{prefix}/$args{path}.pm";
+ $args{wrote} = 0;
+
+ bless( \%args, $class );
+}
+
+sub call {
+ my ($self, $method) = @_;
+ my $obj = $self->load($method) or return;
+ splice(@_, 0, 2, $obj);
+ goto &{$obj->can($method)};
+}
+
+sub load {
+ my ($self, $method) = @_;
+
+ $self->load_extensions(
+ "$self->{prefix}/$self->{path}", $self
+ ) unless $self->{extensions};
+
+ foreach my $obj (@{$self->{extensions}}) {
+ return $obj if $obj->can($method);
+ }
+
+ my $admin = $self->{admin} or die <<"END_DIE";
+The '$method' method does not exist in the '$self->{prefix}' path!
+Please remove the '$self->{prefix}' directory and run $0 again to load it.
+END_DIE
+
+ my $obj = $admin->load($method, 1);
+ push @{$self->{extensions}}, $obj;
+
+ $obj;
+}
+
+sub load_extensions {
+ my ($self, $path, $top) = @_;
+
+ my $should_reload = 0;
+ unless ( grep { ! ref $_ and lc $_ eq lc $self->{prefix} } @INC ) {
+ unshift @INC, $self->{prefix};
+ $should_reload = 1;
+ }
+
+ foreach my $rv ( $self->find_extensions($path) ) {
+ my ($file, $pkg) = @{$rv};
+ next if $self->{pathnames}{$pkg};
+
+ local $@;
+ my $new = eval { local $^W; require $file; $pkg->can('new') };
+ unless ( $new ) {
+ warn $@ if $@;
+ next;
+ }
+ $self->{pathnames}{$pkg} =
+ $should_reload ? delete $INC{$file} : $INC{$file};
+ push @{$self->{extensions}}, &{$new}($pkg, _top => $top );
+ }
+
+ $self->{extensions} ||= [];
+}
+
+sub find_extensions {
+ my ($self, $path) = @_;
+
+ my @found;
+ File::Find::find( {no_chdir => 1, wanted => sub {
+ my $file = $File::Find::name;
+ return unless $file =~ m!^\Q$path\E/(.+)\.pm\Z!is;
+ my $subpath = $1;
+ return if lc($subpath) eq lc($self->{dispatch});
+
+ $file = "$self->{path}/$subpath.pm";
+ my $pkg = "$self->{name}::$subpath";
+ $pkg =~ s!/!::!g;
+
+ # If we have a mixed-case package name, assume case has been preserved
+ # correctly. Otherwise, root through the file to locate the case-preserved
+ # version of the package name.
+ if ( $subpath eq lc($subpath) || $subpath eq uc($subpath) ) {
+ my $content = Module::Install::_read($File::Find::name);
+ my $in_pod = 0;
+ foreach ( split /\n/, $content ) {
+ $in_pod = 1 if /^=\w/;
+ $in_pod = 0 if /^=cut/;
+ next if ($in_pod || /^=cut/); # skip pod text
+ next if /^\s*#/; # and comments
+ if ( m/^\s*package\s+($pkg)\s*;/i ) {
+ $pkg = $1;
+ last;
+ }
+ }
+ }
+
+ push @found, [ $file, $pkg ];
+ }}, $path ) if -d $path;
+
+ @found;
+}
+
+
+
+
+
+#####################################################################
+# Common Utility Functions
+
+sub _caller {
+ my $depth = 0;
+ my $call = caller($depth);
+ while ( $call eq __PACKAGE__ ) {
+ $depth++;
+ $call = caller($depth);
+ }
+ return $call;
+}
+
+sub _read {
+ local *FH;
+ open( FH, '<', $_[0] ) or die "open($_[0]): $!";
+ binmode FH;
+ my $string = do { local $/; <FH> };
+ close FH or die "close($_[0]): $!";
+ return $string;
+}
+
+sub _readperl {
+ my $string = Module::Install::_read($_[0]);
+ $string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+ $string =~ s/(\n)\n*__(?:DATA|END)__\b.*\z/$1/s;
+ $string =~ s/\n\n=\w+.+?\n\n=cut\b.+?\n+/\n\n/sg;
+ return $string;
+}
+
+sub _readpod {
+ my $string = Module::Install::_read($_[0]);
+ $string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+ return $string if $_[0] =~ /\.pod\z/;
+ $string =~ s/(^|\n=cut\b.+?\n+)[^=\s].+?\n(\n=\w+|\z)/$1$2/sg;
+ $string =~ s/\n*=pod\b[^\n]*\n+/\n\n/sg;
+ $string =~ s/\n*=cut\b[^\n]*\n+/\n\n/sg;
+ $string =~ s/^\n+//s;
+ return $string;
+}
+
+sub _write {
+ local *FH;
+ open( FH, '>', $_[0] ) or die "open($_[0]): $!";
+ binmode FH;
+ foreach ( 1 .. $#_ ) {
+ print FH $_[$_] or die "print($_[0]): $!";
+ }
+ close FH or die "close($_[0]): $!";
+}
+
+# _version is for processing module versions (eg, 1.03_05) not
+# Perl versions (eg, 5.8.1).
+sub _version {
+ my $s = shift || 0;
+ my $d =()= $s =~ /(\.)/g;
+ if ( $d >= 2 ) {
+ # Normalise multipart versions
+ $s =~ s/(\.)(\d{1,3})/sprintf("$1%03d",$2)/eg;
+ }
+ $s =~ s/^(\d+)\.?//;
+ my $l = $1 || 0;
+ my @v = map {
+ $_ . '0' x (3 - length $_)
+ } $s =~ /(\d{1,3})\D?/g;
+ $l = $l . '.' . join '', @v if @v;
+ return $l + 0;
+}
+
+sub _cmp {
+ _version($_[1]) <=> _version($_[2]);
+}
+
+# Cloned from Params::Util::_CLASS
+sub _CLASS {
+ (
+ defined $_[0]
+ and
+ ! ref $_[0]
+ and
+ $_[0] =~ m/^[^\W\d]\w*(?:::\w+)*\z/s
+ ) ? $_[0] : undef;
+}
+
+1;
+
+# Copyright 2008 - 2012 Adam Kennedy.
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
new file mode 100644
index 0000000..9fa42c2
--- /dev/null
+++ b/inc/Module/Install/Base.pm
@@ -0,0 +1,83 @@
+#line 1
+package Module::Install::Base;
+
+use strict 'vars';
+use vars qw{$VERSION};
+BEGIN {
+ $VERSION = '1.19';
+}
+
+# Suspend handler for "redefined" warnings
+BEGIN {
+ my $w = $SIG{__WARN__};
+ $SIG{__WARN__} = sub { $w };
+}
+
+#line 42
+
+sub new {
+ my $class = shift;
+ unless ( defined &{"${class}::call"} ) {
+ *{"${class}::call"} = sub { shift->_top->call(@_) };
+ }
+ unless ( defined &{"${class}::load"} ) {
+ *{"${class}::load"} = sub { shift->_top->load(@_) };
+ }
+ bless { @_ }, $class;
+}
+
+#line 61
+
+sub AUTOLOAD {
+ local $@;
+ my $func = eval { shift->_top->autoload } or return;
+ goto &$func;
+}
+
+#line 75
+
+sub _top {
+ $_[0]->{_top};
+}
+
+#line 90
+
+sub admin {
+ $_[0]->_top->{admin}
+ or
+ Module::Install::Base::FakeAdmin->new;
+}
+
+#line 106
+
+sub is_admin {
+ ! $_[0]->admin->isa('Module::Install::Base::FakeAdmin');
+}
+
+sub DESTROY {}
+
+package Module::Install::Base::FakeAdmin;
+
+use vars qw{$VERSION};
+BEGIN {
+ $VERSION = $Module::Install::Base::VERSION;
+}
+
+my $fake;
+
+sub new {
+ $fake ||= bless(\@_, $_[0]);
+}
+
+sub AUTOLOAD {}
+
+sub DESTROY {}
+
+# Restore warning handler
+BEGIN {
+ $SIG{__WARN__} = $SIG{__WARN__}->();
+}
+
+1;
+
+#line 159
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
new file mode 100644
index 0000000..d65c753
--- /dev/null
+++ b/inc/Module/Install/Can.pm
@@ -0,0 +1,163 @@
+#line 1
+package Module::Install::Can;
+
+use strict;
+use Config ();
+use ExtUtils::MakeMaker ();
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+# check if we can load some module
+### Upgrade this to not have to load the module if possible
+sub can_use {
+ my ($self, $mod, $ver) = @_;
+ $mod =~ s{::|\\}{/}g;
+ $mod .= '.pm' unless $mod =~ /\.pm$/i;
+
+ my $pkg = $mod;
+ $pkg =~ s{/}{::}g;
+ $pkg =~ s{\.pm$}{}i;
+
+ local $@;
+ eval { require $mod; $pkg->VERSION($ver || 0); 1 };
+}
+
+# Check if we can run some command
+sub can_run {
+ my ($self, $cmd) = @_;
+
+ my $_cmd = $cmd;
+ return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd));
+
+ for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') {
+ next if $dir eq '';
+ require File::Spec;
+ my $abs = File::Spec->catfile($dir, $cmd);
+ return $abs if (-x $abs or $abs = MM->maybe_command($abs));
+ }
+
+ return;
+}
+
+# Can our C compiler environment build XS files
+sub can_xs {
+ my $self = shift;
+
+ # Ensure we have the CBuilder module
+ $self->configure_requires( 'ExtUtils::CBuilder' => 0.27 );
+
+ # Do we have the configure_requires checker?
+ local $@;
+ eval "require ExtUtils::CBuilder;";
+ if ( $@ ) {
+ # They don't obey configure_requires, so it is
+ # someone old and delicate. Try to avoid hurting
+ # them by falling back to an older simpler test.
+ return $self->can_cc();
+ }
+
+ # Do we have a working C compiler
+ my $builder = ExtUtils::CBuilder->new(
+ quiet => 1,
+ );
+ unless ( $builder->have_compiler ) {
+ # No working C compiler
+ return 0;
+ }
+
+ # Write a C file representative of what XS becomes
+ require File::Temp;
+ my ( $FH, $tmpfile ) = File::Temp::tempfile(
+ "compilexs-XXXXX",
+ SUFFIX => '.c',
+ );
+ binmode $FH;
+ print $FH <<'END_C';
+#include "EXTERN.h"
+#include "perl.h"
+#include "XSUB.h"
+
+int main(int argc, char **argv) {
+ return 0;
+}
+
+int boot_sanexs() {
+ return 1;
+}
+
+END_C
+ close $FH;
+
+ # Can the C compiler access the same headers XS does
+ my @libs = ();
+ my $object = undef;
+ eval {
+ local $^W = 0;
+ $object = $builder->compile(
+ source => $tmpfile,
+ );
+ @libs = $builder->link(
+ objects => $object,
+ module_name => 'sanexs',
+ );
+ };
+ my $result = $@ ? 0 : 1;
+
+ # Clean up all the build files
+ foreach ( $tmpfile, $object, @libs ) {
+ next unless defined $_;
+ 1 while unlink;
+ }
+
+ return $result;
+}
+
+# Can we locate a (the) C compiler
+sub can_cc {
+ my $self = shift;
+
+ if ($^O eq 'VMS') {
+ require ExtUtils::CBuilder;
+ my $builder = ExtUtils::CBuilder->new(
+ quiet => 1,
+ );
+ return $builder->have_compiler;
+ }
+
+ my @chunks = split(/ /, $Config::Config{cc}) or return;
+
+ # $Config{cc} may contain args; try to find out the program part
+ while (@chunks) {
+ return $self->can_run("@chunks") || (pop(@chunks), next);
+ }
+
+ return;
+}
+
+# Fix Cygwin bug on maybe_command();
+if ( $^O eq 'cygwin' ) {
+ require ExtUtils::MM_Cygwin;
+ require ExtUtils::MM_Win32;
+ if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) {
+ *ExtUtils::MM_Cygwin::maybe_command = sub {
+ my ($self, $file) = @_;
+ if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) {
+ ExtUtils::MM_Win32->maybe_command($file);
+ } else {
+ ExtUtils::MM_Unix->maybe_command($file);
+ }
+ }
+ }
+}
+
+1;
+
+__END__
+
+#line 245
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
new file mode 100644
index 0000000..3072b08
--- /dev/null
+++ b/inc/Module/Install/Fetch.pm
@@ -0,0 +1,93 @@
+#line 1
+package Module::Install::Fetch;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+sub get_file {
+ my ($self, %args) = @_;
+ my ($scheme, $host, $path, $file) =
+ $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+
+ if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) {
+ $args{url} = $args{ftp_url}
+ or (warn("LWP support unavailable!\n"), return);
+ ($scheme, $host, $path, $file) =
+ $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+ }
+
+ $|++;
+ print "Fetching '$file' from $host... ";
+
+ unless (eval { require Socket; Socket::inet_aton($host) }) {
+ warn "'$host' resolve failed!\n";
+ return;
+ }
+
+ return unless $scheme eq 'ftp' or $scheme eq 'http';
+
+ require Cwd;
+ my $dir = Cwd::getcwd();
+ chdir $args{local_dir} or return if exists $args{local_dir};
+
+ if (eval { require LWP::Simple; 1 }) {
+ LWP::Simple::mirror($args{url}, $file);
+ }
+ elsif (eval { require Net::FTP; 1 }) { eval {
+ # use Net::FTP to get past firewall
+ my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600);
+ $ftp->login("anonymous", 'anonymous at example.com');
+ $ftp->cwd($path);
+ $ftp->binary;
+ $ftp->get($file) or (warn("$!\n"), return);
+ $ftp->quit;
+ } }
+ elsif (my $ftp = $self->can_run('ftp')) { eval {
+ # no Net::FTP, fallback to ftp.exe
+ require FileHandle;
+ my $fh = FileHandle->new;
+
+ local $SIG{CHLD} = 'IGNORE';
+ unless ($fh->open("|$ftp -n")) {
+ warn "Couldn't open ftp: $!\n";
+ chdir $dir; return;
+ }
+
+ my @dialog = split(/\n/, <<"END_FTP");
+open $host
+user anonymous anonymous\@example.com
+cd $path
+binary
+get $file $file
+quit
+END_FTP
+ foreach (@dialog) { $fh->print("$_\n") }
+ $fh->close;
+ } }
+ else {
+ warn "No working 'ftp' program available!\n";
+ chdir $dir; return;
+ }
+
+ unless (-f $file) {
+ warn "Fetching failed: $@\n";
+ chdir $dir; return;
+ }
+
+ return if exists $args{size} and -s $file != $args{size};
+ system($args{run}) if exists $args{run};
+ unlink($file) if $args{remove};
+
+ print(((!exists $args{check_for} or -e $args{check_for})
+ ? "done!" : "failed! ($!)"), "\n");
+ chdir $dir; return !$?;
+}
+
+1;
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
new file mode 100644
index 0000000..13fdcd0
--- /dev/null
+++ b/inc/Module/Install/Include.pm
@@ -0,0 +1,34 @@
+#line 1
+package Module::Install::Include;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+sub include {
+ shift()->admin->include(@_);
+}
+
+sub include_deps {
+ shift()->admin->include_deps(@_);
+}
+
+sub auto_include {
+ shift()->admin->auto_include(@_);
+}
+
+sub auto_include_deps {
+ shift()->admin->auto_include_deps(@_);
+}
+
+sub auto_include_dependent_dists {
+ shift()->admin->auto_include_dependent_dists(@_);
+}
+
+1;
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
new file mode 100644
index 0000000..13a4464
--- /dev/null
+++ b/inc/Module/Install/Makefile.pm
@@ -0,0 +1,418 @@
+#line 1
+package Module::Install::Makefile;
+
+use strict 'vars';
+use ExtUtils::MakeMaker ();
+use Module::Install::Base ();
+use Fcntl qw/:flock :seek/;
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+sub Makefile { $_[0] }
+
+my %seen = ();
+
+sub prompt {
+ shift;
+
+ # Infinite loop protection
+ my @c = caller();
+ if ( ++$seen{"$c[1]|$c[2]|$_[0]"} > 3 ) {
+ die "Caught an potential prompt infinite loop ($c[1]|$c[2]|$_[0])";
+ }
+
+ # In automated testing or non-interactive session, always use defaults
+ if ( ($ENV{AUTOMATED_TESTING} or -! -t STDIN) and ! $ENV{PERL_MM_USE_DEFAULT} ) {
+ local $ENV{PERL_MM_USE_DEFAULT} = 1;
+ goto &ExtUtils::MakeMaker::prompt;
+ } else {
+ goto &ExtUtils::MakeMaker::prompt;
+ }
+}
+
+# Store a cleaned up version of the MakeMaker version,
+# since we need to behave differently in a variety of
+# ways based on the MM version.
+my $makemaker = eval $ExtUtils::MakeMaker::VERSION;
+
+# If we are passed a param, do a "newer than" comparison.
+# Otherwise, just return the MakeMaker version.
+sub makemaker {
+ ( @_ < 2 or $makemaker >= eval($_[1]) ) ? $makemaker : 0
+}
+
+# Ripped from ExtUtils::MakeMaker 6.56, and slightly modified
+# as we only need to know here whether the attribute is an array
+# or a hash or something else (which may or may not be appendable).
+my %makemaker_argtype = (
+ C => 'ARRAY',
+ CONFIG => 'ARRAY',
+# CONFIGURE => 'CODE', # ignore
+ DIR => 'ARRAY',
+ DL_FUNCS => 'HASH',
+ DL_VARS => 'ARRAY',
+ EXCLUDE_EXT => 'ARRAY',
+ EXE_FILES => 'ARRAY',
+ FUNCLIST => 'ARRAY',
+ H => 'ARRAY',
+ IMPORTS => 'HASH',
+ INCLUDE_EXT => 'ARRAY',
+ LIBS => 'ARRAY', # ignore ''
+ MAN1PODS => 'HASH',
+ MAN3PODS => 'HASH',
+ META_ADD => 'HASH',
+ META_MERGE => 'HASH',
+ PL_FILES => 'HASH',
+ PM => 'HASH',
+ PMLIBDIRS => 'ARRAY',
+ PMLIBPARENTDIRS => 'ARRAY',
+ PREREQ_PM => 'HASH',
+ CONFIGURE_REQUIRES => 'HASH',
+ SKIP => 'ARRAY',
+ TYPEMAPS => 'ARRAY',
+ XS => 'HASH',
+# VERSION => ['version',''], # ignore
+# _KEEP_AFTER_FLUSH => '',
+
+ clean => 'HASH',
+ depend => 'HASH',
+ dist => 'HASH',
+ dynamic_lib=> 'HASH',
+ linkext => 'HASH',
+ macro => 'HASH',
+ postamble => 'HASH',
+ realclean => 'HASH',
+ test => 'HASH',
+ tool_autosplit => 'HASH',
+
+ # special cases where you can use makemaker_append
+ CCFLAGS => 'APPENDABLE',
+ DEFINE => 'APPENDABLE',
+ INC => 'APPENDABLE',
+ LDDLFLAGS => 'APPENDABLE',
+ LDFROM => 'APPENDABLE',
+);
+
+sub makemaker_args {
+ my ($self, %new_args) = @_;
+ my $args = ( $self->{makemaker_args} ||= {} );
+ foreach my $key (keys %new_args) {
+ if ($makemaker_argtype{$key}) {
+ if ($makemaker_argtype{$key} eq 'ARRAY') {
+ $args->{$key} = [] unless defined $args->{$key};
+ unless (ref $args->{$key} eq 'ARRAY') {
+ $args->{$key} = [$args->{$key}]
+ }
+ push @{$args->{$key}},
+ ref $new_args{$key} eq 'ARRAY'
+ ? @{$new_args{$key}}
+ : $new_args{$key};
+ }
+ elsif ($makemaker_argtype{$key} eq 'HASH') {
+ $args->{$key} = {} unless defined $args->{$key};
+ foreach my $skey (keys %{ $new_args{$key} }) {
+ $args->{$key}{$skey} = $new_args{$key}{$skey};
+ }
+ }
+ elsif ($makemaker_argtype{$key} eq 'APPENDABLE') {
+ $self->makemaker_append($key => $new_args{$key});
+ }
+ }
+ else {
+ if (defined $args->{$key}) {
+ warn qq{MakeMaker attribute "$key" is overriden; use "makemaker_append" to append values\n};
+ }
+ $args->{$key} = $new_args{$key};
+ }
+ }
+ return $args;
+}
+
+# For mm args that take multiple space-separated args,
+# append an argument to the current list.
+sub makemaker_append {
+ my $self = shift;
+ my $name = shift;
+ my $args = $self->makemaker_args;
+ $args->{$name} = defined $args->{$name}
+ ? join( ' ', $args->{$name}, @_ )
+ : join( ' ', @_ );
+}
+
+sub build_subdirs {
+ my $self = shift;
+ my $subdirs = $self->makemaker_args->{DIR} ||= [];
+ for my $subdir (@_) {
+ push @$subdirs, $subdir;
+ }
+}
+
+sub clean_files {
+ my $self = shift;
+ my $clean = $self->makemaker_args->{clean} ||= {};
+ %$clean = (
+ %$clean,
+ FILES => join ' ', grep { length $_ } ($clean->{FILES} || (), @_),
+ );
+}
+
+sub realclean_files {
+ my $self = shift;
+ my $realclean = $self->makemaker_args->{realclean} ||= {};
+ %$realclean = (
+ %$realclean,
+ FILES => join ' ', grep { length $_ } ($realclean->{FILES} || (), @_),
+ );
+}
+
+sub libs {
+ my $self = shift;
+ my $libs = ref $_[0] ? shift : [ shift ];
+ $self->makemaker_args( LIBS => $libs );
+}
+
+sub inc {
+ my $self = shift;
+ $self->makemaker_args( INC => shift );
+}
+
+sub _wanted_t {
+}
+
+sub tests_recursive {
+ my $self = shift;
+ my $dir = shift || 't';
+ unless ( -d $dir ) {
+ die "tests_recursive dir '$dir' does not exist";
+ }
+ my %tests = map { $_ => 1 } split / /, ($self->tests || '');
+ require File::Find;
+ File::Find::find(
+ sub { /\.t$/ and -f $_ and $tests{"$File::Find::dir/*.t"} = 1 },
+ $dir
+ );
+ $self->tests( join ' ', sort keys %tests );
+}
+
+sub write {
+ my $self = shift;
+ die "&Makefile->write() takes no arguments\n" if @_;
+
+ # Check the current Perl version
+ my $perl_version = $self->perl_version;
+ if ( $perl_version ) {
+ eval "use $perl_version; 1"
+ or die "ERROR: perl: Version $] is installed, "
+ . "but we need version >= $perl_version";
+ }
+
+ # Make sure we have a new enough MakeMaker
+ require ExtUtils::MakeMaker;
+
+ if ( $perl_version and $self->_cmp($perl_version, '5.006') >= 0 ) {
+ # This previous attempted to inherit the version of
+ # ExtUtils::MakeMaker in use by the module author, but this
+ # was found to be untenable as some authors build releases
+ # using future dev versions of EU:MM that nobody else has.
+ # Instead, #toolchain suggests we use 6.59 which is the most
+ # stable version on CPAN at time of writing and is, to quote
+ # ribasushi, "not terminally fucked, > and tested enough".
+ # TODO: We will now need to maintain this over time to push
+ # the version up as new versions are released.
+ $self->build_requires( 'ExtUtils::MakeMaker' => 6.59 );
+ $self->configure_requires( 'ExtUtils::MakeMaker' => 6.59 );
+ } else {
+ # Allow legacy-compatibility with 5.005 by depending on the
+ # most recent EU:MM that supported 5.005.
+ $self->build_requires( 'ExtUtils::MakeMaker' => 6.36 );
+ $self->configure_requires( 'ExtUtils::MakeMaker' => 6.36 );
+ }
+
+ # Generate the MakeMaker params
+ my $args = $self->makemaker_args;
+ $args->{DISTNAME} = $self->name;
+ $args->{NAME} = $self->module_name || $self->name;
+ $args->{NAME} =~ s/-/::/g;
+ $args->{VERSION} = $self->version or die <<'EOT';
+ERROR: Can't determine distribution version. Please specify it
+explicitly via 'version' in Makefile.PL, or set a valid $VERSION
+in a module, and provide its file path via 'version_from' (or
+'all_from' if you prefer) in Makefile.PL.
+EOT
+
+ if ( $self->tests ) {
+ my @tests = split ' ', $self->tests;
+ my %seen;
+ $args->{test} = {
+ TESTS => (join ' ', grep {!$seen{$_}++} @tests),
+ };
+ } elsif ( $Module::Install::ExtraTests::use_extratests ) {
+ # Module::Install::ExtraTests doesn't set $self->tests and does its own tests via harness.
+ # So, just ignore our xt tests here.
+ } elsif ( -d 'xt' and ($Module::Install::AUTHOR or $ENV{RELEASE_TESTING}) ) {
+ $args->{test} = {
+ TESTS => join( ' ', map { "$_/*.t" } grep { -d $_ } qw{ t xt } ),
+ };
+ }
+ if ( $] >= 5.005 ) {
+ $args->{ABSTRACT} = $self->abstract;
+ $args->{AUTHOR} = join ', ', @{$self->author || []};
+ }
+ if ( $self->makemaker(6.10) ) {
+ $args->{NO_META} = 1;
+ #$args->{NO_MYMETA} = 1;
+ }
+ if ( $self->makemaker(6.17) and $self->sign ) {
+ $args->{SIGN} = 1;
+ }
+ unless ( $self->is_admin ) {
+ delete $args->{SIGN};
+ }
+ if ( $self->makemaker(6.31) and $self->license ) {
+ $args->{LICENSE} = $self->license;
+ }
+
+ my $prereq = ($args->{PREREQ_PM} ||= {});
+ %$prereq = ( %$prereq,
+ map { @$_ } # flatten [module => version]
+ map { @$_ }
+ grep $_,
+ ($self->requires)
+ );
+
+ # Remove any reference to perl, PREREQ_PM doesn't support it
+ delete $args->{PREREQ_PM}->{perl};
+
+ # Merge both kinds of requires into BUILD_REQUIRES
+ my $build_prereq = ($args->{BUILD_REQUIRES} ||= {});
+ %$build_prereq = ( %$build_prereq,
+ map { @$_ } # flatten [module => version]
+ map { @$_ }
+ grep $_,
+ ($self->configure_requires, $self->build_requires)
+ );
+
+ # Remove any reference to perl, BUILD_REQUIRES doesn't support it
+ delete $args->{BUILD_REQUIRES}->{perl};
+
+ # Delete bundled dists from prereq_pm, add it to Makefile DIR
+ my $subdirs = ($args->{DIR} || []);
+ if ($self->bundles) {
+ my %processed;
+ foreach my $bundle (@{ $self->bundles }) {
+ my ($mod_name, $dist_dir) = @$bundle;
+ delete $prereq->{$mod_name};
+ $dist_dir = File::Basename::basename($dist_dir); # dir for building this module
+ if (not exists $processed{$dist_dir}) {
+ if (-d $dist_dir) {
+ # List as sub-directory to be processed by make
+ push @$subdirs, $dist_dir;
+ }
+ # Else do nothing: the module is already present on the system
+ $processed{$dist_dir} = undef;
+ }
+ }
+ }
+
+ unless ( $self->makemaker('6.55_03') ) {
+ %$prereq = (%$prereq,%$build_prereq);
+ delete $args->{BUILD_REQUIRES};
+ }
+
+ if ( my $perl_version = $self->perl_version ) {
+ eval "use $perl_version; 1"
+ or die "ERROR: perl: Version $] is installed, "
+ . "but we need version >= $perl_version";
+
+ if ( $self->makemaker(6.48) ) {
+ $args->{MIN_PERL_VERSION} = $perl_version;
+ }
+ }
+
+ if ($self->installdirs) {
+ warn qq{old INSTALLDIRS (probably set by makemaker_args) is overriden by installdirs\n} if $args->{INSTALLDIRS};
+ $args->{INSTALLDIRS} = $self->installdirs;
+ }
+
+ my %args = map {
+ ( $_ => $args->{$_} ) } grep {defined($args->{$_} )
+ } keys %$args;
+
+ my $user_preop = delete $args{dist}->{PREOP};
+ if ( my $preop = $self->admin->preop($user_preop) ) {
+ foreach my $key ( keys %$preop ) {
+ $args{dist}->{$key} = $preop->{$key};
+ }
+ }
+
+ my $mm = ExtUtils::MakeMaker::WriteMakefile(%args);
+ $self->fix_up_makefile($mm->{FIRST_MAKEFILE} || 'Makefile');
+}
+
+sub fix_up_makefile {
+ my $self = shift;
+ my $makefile_name = shift;
+ my $top_class = ref($self->_top) || '';
+ my $top_version = $self->_top->VERSION || '';
+
+ my $preamble = $self->preamble
+ ? "# Preamble by $top_class $top_version\n"
+ . $self->preamble
+ : '';
+ my $postamble = "# Postamble by $top_class $top_version\n"
+ . ($self->postamble || '');
+
+ local *MAKEFILE;
+ open MAKEFILE, "+< $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!";
+ eval { flock MAKEFILE, LOCK_EX };
+ my $makefile = do { local $/; <MAKEFILE> };
+
+ $makefile =~ s/\b(test_harness\(\$\(TEST_VERBOSE\), )/$1'inc', /;
+ $makefile =~ s/( -I\$\(INST_ARCHLIB\))/ -Iinc$1/g;
+ $makefile =~ s/( "-I\$\(INST_LIB\)")/ "-Iinc"$1/g;
+ $makefile =~ s/^(FULLPERL = .*)/$1 "-Iinc"/m;
+ $makefile =~ s/^(PERL = .*)/$1 "-Iinc"/m;
+
+ # Module::Install will never be used to build the Core Perl
+ # Sometimes PERL_LIB and PERL_ARCHLIB get written anyway, which breaks
+ # PREFIX/PERL5LIB, and thus, install_share. Blank them if they exist
+ $makefile =~ s/^PERL_LIB = .+/PERL_LIB =/m;
+ #$makefile =~ s/^PERL_ARCHLIB = .+/PERL_ARCHLIB =/m;
+
+ # Perl 5.005 mentions PERL_LIB explicitly, so we have to remove that as well.
+ $makefile =~ s/(\"?)-I\$\(PERL_LIB\)\1//g;
+
+ # XXX - This is currently unused; not sure if it breaks other MM-users
+ # $makefile =~ s/^pm_to_blib\s+:\s+/pm_to_blib :: /mg;
+
+ seek MAKEFILE, 0, SEEK_SET;
+ truncate MAKEFILE, 0;
+ print MAKEFILE "$preamble$makefile$postamble" or die $!;
+ close MAKEFILE or die $!;
+
+ 1;
+}
+
+sub preamble {
+ my ($self, $text) = @_;
+ $self->{preamble} = $text . $self->{preamble} if defined $text;
+ $self->{preamble};
+}
+
+sub postamble {
+ my ($self, $text) = @_;
+ $self->{postamble} ||= $self->admin->postamble;
+ $self->{postamble} .= $text if defined $text;
+ $self->{postamble}
+}
+
+1;
+
+__END__
+
+#line 544
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
new file mode 100644
index 0000000..11bf971
--- /dev/null
+++ b/inc/Module/Install/Metadata.pm
@@ -0,0 +1,722 @@
+#line 1
+package Module::Install::Metadata;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+my @boolean_keys = qw{
+ sign
+};
+
+my @scalar_keys = qw{
+ name
+ module_name
+ abstract
+ version
+ distribution_type
+ tests
+ installdirs
+};
+
+my @tuple_keys = qw{
+ configure_requires
+ build_requires
+ requires
+ recommends
+ bundles
+ resources
+};
+
+my @resource_keys = qw{
+ homepage
+ bugtracker
+ repository
+};
+
+my @array_keys = qw{
+ keywords
+ author
+};
+
+*authors = \&author;
+
+sub Meta { shift }
+sub Meta_BooleanKeys { @boolean_keys }
+sub Meta_ScalarKeys { @scalar_keys }
+sub Meta_TupleKeys { @tuple_keys }
+sub Meta_ResourceKeys { @resource_keys }
+sub Meta_ArrayKeys { @array_keys }
+
+foreach my $key ( @boolean_keys ) {
+ *$key = sub {
+ my $self = shift;
+ if ( defined wantarray and not @_ ) {
+ return $self->{values}->{$key};
+ }
+ $self->{values}->{$key} = ( @_ ? $_[0] : 1 );
+ return $self;
+ };
+}
+
+foreach my $key ( @scalar_keys ) {
+ *$key = sub {
+ my $self = shift;
+ return $self->{values}->{$key} if defined wantarray and !@_;
+ $self->{values}->{$key} = shift;
+ return $self;
+ };
+}
+
+foreach my $key ( @array_keys ) {
+ *$key = sub {
+ my $self = shift;
+ return $self->{values}->{$key} if defined wantarray and !@_;
+ $self->{values}->{$key} ||= [];
+ push @{$self->{values}->{$key}}, @_;
+ return $self;
+ };
+}
+
+foreach my $key ( @resource_keys ) {
+ *$key = sub {
+ my $self = shift;
+ unless ( @_ ) {
+ return () unless $self->{values}->{resources};
+ return map { $_->[1] }
+ grep { $_->[0] eq $key }
+ @{ $self->{values}->{resources} };
+ }
+ return $self->{values}->{resources}->{$key} unless @_;
+ my $uri = shift or die(
+ "Did not provide a value to $key()"
+ );
+ $self->resources( $key => $uri );
+ return 1;
+ };
+}
+
+foreach my $key ( grep { $_ ne "resources" } @tuple_keys) {
+ *$key = sub {
+ my $self = shift;
+ return $self->{values}->{$key} unless @_;
+ my @added;
+ while ( @_ ) {
+ my $module = shift or last;
+ my $version = shift || 0;
+ push @added, [ $module, $version ];
+ }
+ push @{ $self->{values}->{$key} }, @added;
+ return map {@$_} @added;
+ };
+}
+
+# Resource handling
+my %lc_resource = map { $_ => 1 } qw{
+ homepage
+ license
+ bugtracker
+ repository
+};
+
+sub resources {
+ my $self = shift;
+ while ( @_ ) {
+ my $name = shift or last;
+ my $value = shift or next;
+ if ( $name eq lc $name and ! $lc_resource{$name} ) {
+ die("Unsupported reserved lowercase resource '$name'");
+ }
+ $self->{values}->{resources} ||= [];
+ push @{ $self->{values}->{resources} }, [ $name, $value ];
+ }
+ $self->{values}->{resources};
+}
+
+# Aliases for build_requires that will have alternative
+# meanings in some future version of META.yml.
+sub test_requires { shift->build_requires(@_) }
+sub install_requires { shift->build_requires(@_) }
+
+# Aliases for installdirs options
+sub install_as_core { $_[0]->installdirs('perl') }
+sub install_as_cpan { $_[0]->installdirs('site') }
+sub install_as_site { $_[0]->installdirs('site') }
+sub install_as_vendor { $_[0]->installdirs('vendor') }
+
+sub dynamic_config {
+ my $self = shift;
+ my $value = @_ ? shift : 1;
+ if ( $self->{values}->{dynamic_config} ) {
+ # Once dynamic we never change to static, for safety
+ return 0;
+ }
+ $self->{values}->{dynamic_config} = $value ? 1 : 0;
+ return 1;
+}
+
+# Convenience command
+sub static_config {
+ shift->dynamic_config(0);
+}
+
+sub perl_version {
+ my $self = shift;
+ return $self->{values}->{perl_version} unless @_;
+ my $version = shift or die(
+ "Did not provide a value to perl_version()"
+ );
+
+ # Normalize the version
+ $version = $self->_perl_version($version);
+
+ # We don't support the really old versions
+ unless ( $version >= 5.005 ) {
+ die "Module::Install only supports 5.005 or newer (use ExtUtils::MakeMaker)\n";
+ }
+
+ $self->{values}->{perl_version} = $version;
+}
+
+sub all_from {
+ my ( $self, $file ) = @_;
+
+ unless ( defined($file) ) {
+ my $name = $self->name or die(
+ "all_from called with no args without setting name() first"
+ );
+ $file = join('/', 'lib', split(/-/, $name)) . '.pm';
+ $file =~ s{.*/}{} unless -e $file;
+ unless ( -e $file ) {
+ die("all_from cannot find $file from $name");
+ }
+ }
+ unless ( -f $file ) {
+ die("The path '$file' does not exist, or is not a file");
+ }
+
+ $self->{values}{all_from} = $file;
+
+ # Some methods pull from POD instead of code.
+ # If there is a matching .pod, use that instead
+ my $pod = $file;
+ $pod =~ s/\.pm$/.pod/i;
+ $pod = $file unless -e $pod;
+
+ # Pull the different values
+ $self->name_from($file) unless $self->name;
+ $self->version_from($file) unless $self->version;
+ $self->perl_version_from($file) unless $self->perl_version;
+ $self->author_from($pod) unless @{$self->author || []};
+ $self->license_from($pod) unless $self->license;
+ $self->abstract_from($pod) unless $self->abstract;
+
+ return 1;
+}
+
+sub provides {
+ my $self = shift;
+ my $provides = ( $self->{values}->{provides} ||= {} );
+ %$provides = (%$provides, @_) if @_;
+ return $provides;
+}
+
+sub auto_provides {
+ my $self = shift;
+ return $self unless $self->is_admin;
+ unless (-e 'MANIFEST') {
+ warn "Cannot deduce auto_provides without a MANIFEST, skipping\n";
+ return $self;
+ }
+ # Avoid spurious warnings as we are not checking manifest here.
+ local $SIG{__WARN__} = sub {1};
+ require ExtUtils::Manifest;
+ local *ExtUtils::Manifest::manicheck = sub { return };
+
+ require Module::Build;
+ my $build = Module::Build->new(
+ dist_name => $self->name,
+ dist_version => $self->version,
+ license => $self->license,
+ );
+ $self->provides( %{ $build->find_dist_packages || {} } );
+}
+
+sub feature {
+ my $self = shift;
+ my $name = shift;
+ my $features = ( $self->{values}->{features} ||= [] );
+ my $mods;
+
+ if ( @_ == 1 and ref( $_[0] ) ) {
+ # The user used ->feature like ->features by passing in the second
+ # argument as a reference. Accomodate for that.
+ $mods = $_[0];
+ } else {
+ $mods = \@_;
+ }
+
+ my $count = 0;
+ push @$features, (
+ $name => [
+ map {
+ ref($_) ? ( ref($_) eq 'HASH' ) ? %$_ : @$_ : $_
+ } @$mods
+ ]
+ );
+
+ return @$features;
+}
+
+sub features {
+ my $self = shift;
+ while ( my ( $name, $mods ) = splice( @_, 0, 2 ) ) {
+ $self->feature( $name, @$mods );
+ }
+ return $self->{values}->{features}
+ ? @{ $self->{values}->{features} }
+ : ();
+}
+
+sub no_index {
+ my $self = shift;
+ my $type = shift;
+ push @{ $self->{values}->{no_index}->{$type} }, @_ if $type;
+ return $self->{values}->{no_index};
+}
+
+sub read {
+ my $self = shift;
+ $self->include_deps( 'YAML::Tiny', 0 );
+
+ require YAML::Tiny;
+ my $data = YAML::Tiny::LoadFile('META.yml');
+
+ # Call methods explicitly in case user has already set some values.
+ while ( my ( $key, $value ) = each %$data ) {
+ next unless $self->can($key);
+ if ( ref $value eq 'HASH' ) {
+ while ( my ( $module, $version ) = each %$value ) {
+ $self->can($key)->($self, $module => $version );
+ }
+ } else {
+ $self->can($key)->($self, $value);
+ }
+ }
+ return $self;
+}
+
+sub write {
+ my $self = shift;
+ return $self unless $self->is_admin;
+ $self->admin->write_meta;
+ return $self;
+}
+
+sub version_from {
+ require ExtUtils::MM_Unix;
+ my ( $self, $file ) = @_;
+ $self->version( ExtUtils::MM_Unix->parse_version($file) );
+
+ # for version integrity check
+ $self->makemaker_args( VERSION_FROM => $file );
+}
+
+sub abstract_from {
+ require ExtUtils::MM_Unix;
+ my ( $self, $file ) = @_;
+ $self->abstract(
+ bless(
+ { DISTNAME => $self->name },
+ 'ExtUtils::MM_Unix'
+ )->parse_abstract($file)
+ );
+}
+
+# Add both distribution and module name
+sub name_from {
+ my ($self, $file) = @_;
+ if (
+ Module::Install::_read($file) =~ m/
+ ^ \s*
+ package \s*
+ ([\w:]+)
+ [\s|;]*
+ /ixms
+ ) {
+ my ($name, $module_name) = ($1, $1);
+ $name =~ s{::}{-}g;
+ $self->name($name);
+ unless ( $self->module_name ) {
+ $self->module_name($module_name);
+ }
+ } else {
+ die("Cannot determine name from $file\n");
+ }
+}
+
+sub _extract_perl_version {
+ if (
+ $_[0] =~ m/
+ ^\s*
+ (?:use|require) \s*
+ v?
+ ([\d_\.]+)
+ \s* ;
+ /ixms
+ ) {
+ my $perl_version = $1;
+ $perl_version =~ s{_}{}g;
+ return $perl_version;
+ } else {
+ return;
+ }
+}
+
+sub perl_version_from {
+ my $self = shift;
+ my $perl_version=_extract_perl_version(Module::Install::_read($_[0]));
+ if ($perl_version) {
+ $self->perl_version($perl_version);
+ } else {
+ warn "Cannot determine perl version info from $_[0]\n";
+ return;
+ }
+}
+
+sub author_from {
+ my $self = shift;
+ my $content = Module::Install::_read($_[0]);
+ if ($content =~ m/
+ =head \d \s+ (?:authors?)\b \s*
+ ([^\n]*)
+ |
+ =head \d \s+ (?:licen[cs]e|licensing|copyright|legal)\b \s*
+ .*? copyright .*? \d\d\d[\d.]+ \s* (?:\bby\b)? \s*
+ ([^\n]*)
+ /ixms) {
+ my $author = $1 || $2;
+
+ # XXX: ugly but should work anyway...
+ if (eval "require Pod::Escapes; 1") {
+ # Pod::Escapes has a mapping table.
+ # It's in core of perl >= 5.9.3, and should be installed
+ # as one of the Pod::Simple's prereqs, which is a prereq
+ # of Pod::Text 3.x (see also below).
+ $author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+ {
+ defined $2
+ ? chr($2)
+ : defined $Pod::Escapes::Name2character_number{$1}
+ ? chr($Pod::Escapes::Name2character_number{$1})
+ : do {
+ warn "Unknown escape: E<$1>";
+ "E<$1>";
+ };
+ }gex;
+ }
+ elsif (eval "require Pod::Text; 1" && $Pod::Text::VERSION < 3) {
+ # Pod::Text < 3.0 has yet another mapping table,
+ # though the table name of 2.x and 1.x are different.
+ # (1.x is in core of Perl < 5.6, 2.x is in core of
+ # Perl < 5.9.3)
+ my $mapping = ($Pod::Text::VERSION < 2)
+ ? \%Pod::Text::HTML_Escapes
+ : \%Pod::Text::ESCAPES;
+ $author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+ {
+ defined $2
+ ? chr($2)
+ : defined $mapping->{$1}
+ ? $mapping->{$1}
+ : do {
+ warn "Unknown escape: E<$1>";
+ "E<$1>";
+ };
+ }gex;
+ }
+ else {
+ $author =~ s{E<lt>}{<}g;
+ $author =~ s{E<gt>}{>}g;
+ }
+ $self->author($author);
+ } else {
+ warn "Cannot determine author info from $_[0]\n";
+ }
+}
+
+#Stolen from M::B
+my %license_urls = (
+ perl => 'http://dev.perl.org/licenses/',
+ apache => 'http://apache.org/licenses/LICENSE-2.0',
+ apache_1_1 => 'http://apache.org/licenses/LICENSE-1.1',
+ artistic => 'http://opensource.org/licenses/artistic-license.php',
+ artistic_2 => 'http://opensource.org/licenses/artistic-license-2.0.php',
+ lgpl => 'http://opensource.org/licenses/lgpl-license.php',
+ lgpl2 => 'http://opensource.org/licenses/lgpl-2.1.php',
+ lgpl3 => 'http://opensource.org/licenses/lgpl-3.0.html',
+ bsd => 'http://opensource.org/licenses/bsd-license.php',
+ gpl => 'http://opensource.org/licenses/gpl-license.php',
+ gpl2 => 'http://opensource.org/licenses/gpl-2.0.php',
+ gpl3 => 'http://opensource.org/licenses/gpl-3.0.html',
+ mit => 'http://opensource.org/licenses/mit-license.php',
+ mozilla => 'http://opensource.org/licenses/mozilla1.1.php',
+ open_source => undef,
+ unrestricted => undef,
+ restrictive => undef,
+ unknown => undef,
+);
+
+sub license {
+ my $self = shift;
+ return $self->{values}->{license} unless @_;
+ my $license = shift or die(
+ 'Did not provide a value to license()'
+ );
+ $license = __extract_license($license) || lc $license;
+ $self->{values}->{license} = $license;
+
+ # Automatically fill in license URLs
+ if ( $license_urls{$license} ) {
+ $self->resources( license => $license_urls{$license} );
+ }
+
+ return 1;
+}
+
+sub _extract_license {
+ my $pod = shift;
+ my $matched;
+ return __extract_license(
+ ($matched) = $pod =~ m/
+ (=head \d \s+ L(?i:ICEN[CS]E|ICENSING)\b.*?)
+ (=head \d.*|=cut.*|)\z
+ /xms
+ ) || __extract_license(
+ ($matched) = $pod =~ m/
+ (=head \d \s+ (?:C(?i:OPYRIGHTS?)|L(?i:EGAL))\b.*?)
+ (=head \d.*|=cut.*|)\z
+ /xms
+ );
+}
+
+sub __extract_license {
+ my $license_text = shift or return;
+ my @phrases = (
+ '(?:under )?the same (?:terms|license) as (?:perl|the perl (?:\d )?programming language)' => 'perl', 1,
+ '(?:under )?the terms of (?:perl|the perl programming language) itself' => 'perl', 1,
+ 'Artistic and GPL' => 'perl', 1,
+ 'GNU general public license' => 'gpl', 1,
+ 'GNU public license' => 'gpl', 1,
+ 'GNU lesser general public license' => 'lgpl', 1,
+ 'GNU lesser public license' => 'lgpl', 1,
+ 'GNU library general public license' => 'lgpl', 1,
+ 'GNU library public license' => 'lgpl', 1,
+ 'GNU Free Documentation license' => 'unrestricted', 1,
+ 'GNU Affero General Public License' => 'open_source', 1,
+ '(?:Free)?BSD license' => 'bsd', 1,
+ 'Artistic license 2\.0' => 'artistic_2', 1,
+ 'Artistic license' => 'artistic', 1,
+ 'Apache (?:Software )?license' => 'apache', 1,
+ 'GPL' => 'gpl', 1,
+ 'LGPL' => 'lgpl', 1,
+ 'BSD' => 'bsd', 1,
+ 'Artistic' => 'artistic', 1,
+ 'MIT' => 'mit', 1,
+ 'Mozilla Public License' => 'mozilla', 1,
+ 'Q Public License' => 'open_source', 1,
+ 'OpenSSL License' => 'unrestricted', 1,
+ 'SSLeay License' => 'unrestricted', 1,
+ 'zlib License' => 'open_source', 1,
+ 'proprietary' => 'proprietary', 0,
+ );
+ while ( my ($pattern, $license, $osi) = splice(@phrases, 0, 3) ) {
+ $pattern =~ s#\s+#\\s+#gs;
+ if ( $license_text =~ /\b$pattern\b/i ) {
+ return $license;
+ }
+ }
+ return '';
+}
+
+sub license_from {
+ my $self = shift;
+ if (my $license=_extract_license(Module::Install::_read($_[0]))) {
+ $self->license($license);
+ } else {
+ warn "Cannot determine license info from $_[0]\n";
+ return 'unknown';
+ }
+}
+
+sub _extract_bugtracker {
+ my @links = $_[0] =~ m#L<(
+ https?\Q://rt.cpan.org/\E[^>]+|
+ https?\Q://github.com/\E[\w_]+/[\w_]+/issues|
+ https?\Q://code.google.com/p/\E[\w_\-]+/issues/list
+ )>#gx;
+ my %links;
+ @links{@links}=();
+ @links=keys %links;
+ return @links;
+}
+
+sub bugtracker_from {
+ my $self = shift;
+ my $content = Module::Install::_read($_[0]);
+ my @links = _extract_bugtracker($content);
+ unless ( @links ) {
+ warn "Cannot determine bugtracker info from $_[0]\n";
+ return 0;
+ }
+ if ( @links > 1 ) {
+ warn "Found more than one bugtracker link in $_[0]\n";
+ return 0;
+ }
+
+ # Set the bugtracker
+ bugtracker( $links[0] );
+ return 1;
+}
+
+sub requires_from {
+ my $self = shift;
+ my $content = Module::Install::_readperl($_[0]);
+ my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+(v?[\d\.]+)/mg;
+ while ( @requires ) {
+ my $module = shift @requires;
+ my $version = shift @requires;
+ $self->requires( $module => $version );
+ }
+}
+
+sub test_requires_from {
+ my $self = shift;
+ my $content = Module::Install::_readperl($_[0]);
+ my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+([\d\.]+)/mg;
+ while ( @requires ) {
+ my $module = shift @requires;
+ my $version = shift @requires;
+ $self->test_requires( $module => $version );
+ }
+}
+
+# Convert triple-part versions (eg, 5.6.1 or 5.8.9) to
+# numbers (eg, 5.006001 or 5.008009).
+# Also, convert double-part versions (eg, 5.8)
+sub _perl_version {
+ my $v = $_[-1];
+ $v =~ s/^([1-9])\.([1-9]\d?\d?)$/sprintf("%d.%03d",$1,$2)/e;
+ $v =~ s/^([1-9])\.([1-9]\d?\d?)\.(0|[1-9]\d?\d?)$/sprintf("%d.%03d%03d",$1,$2,$3 || 0)/e;
+ $v =~ s/(\.\d\d\d)000$/$1/;
+ $v =~ s/_.+$//;
+ if ( ref($v) ) {
+ # Numify
+ $v = $v + 0;
+ }
+ return $v;
+}
+
+sub add_metadata {
+ my $self = shift;
+ my %hash = @_;
+ for my $key (keys %hash) {
+ warn "add_metadata: $key is not prefixed with 'x_'.\n" .
+ "Use appopriate function to add non-private metadata.\n" unless $key =~ /^x_/;
+ $self->{values}->{$key} = $hash{$key};
+ }
+}
+
+
+######################################################################
+# MYMETA Support
+
+sub WriteMyMeta {
+ die "WriteMyMeta has been deprecated";
+}
+
+sub write_mymeta_yaml {
+ my $self = shift;
+
+ # We need YAML::Tiny to write the MYMETA.yml file
+ unless ( eval { require YAML::Tiny; 1; } ) {
+ return 1;
+ }
+
+ # Generate the data
+ my $meta = $self->_write_mymeta_data or return 1;
+
+ # Save as the MYMETA.yml file
+ print "Writing MYMETA.yml\n";
+ YAML::Tiny::DumpFile('MYMETA.yml', $meta);
+}
+
+sub write_mymeta_json {
+ my $self = shift;
+
+ # We need JSON to write the MYMETA.json file
+ unless ( eval { require JSON; 1; } ) {
+ return 1;
+ }
+
+ # Generate the data
+ my $meta = $self->_write_mymeta_data or return 1;
+
+ # Save as the MYMETA.yml file
+ print "Writing MYMETA.json\n";
+ Module::Install::_write(
+ 'MYMETA.json',
+ JSON->new->pretty(1)->canonical->encode($meta),
+ );
+}
+
+sub _write_mymeta_data {
+ my $self = shift;
+
+ # If there's no existing META.yml there is nothing we can do
+ return undef unless -f 'META.yml';
+
+ # We need Parse::CPAN::Meta to load the file
+ unless ( eval { require Parse::CPAN::Meta; 1; } ) {
+ return undef;
+ }
+
+ # Merge the perl version into the dependencies
+ my $val = $self->Meta->{values};
+ my $perl = delete $val->{perl_version};
+ if ( $perl ) {
+ $val->{requires} ||= [];
+ my $requires = $val->{requires};
+
+ # Canonize to three-dot version after Perl 5.6
+ if ( $perl >= 5.006 ) {
+ $perl =~ s{^(\d+)\.(\d\d\d)(\d*)}{join('.', $1, int($2||0), int($3||0))}e
+ }
+ unshift @$requires, [ perl => $perl ];
+ }
+
+ # Load the advisory META.yml file
+ my @yaml = Parse::CPAN::Meta::LoadFile('META.yml');
+ my $meta = $yaml[0];
+
+ # Overwrite the non-configure dependency hashes
+ delete $meta->{requires};
+ delete $meta->{build_requires};
+ delete $meta->{recommends};
+ if ( exists $val->{requires} ) {
+ $meta->{requires} = { map { @$_ } @{ $val->{requires} } };
+ }
+ if ( exists $val->{build_requires} ) {
+ $meta->{build_requires} = { map { @$_ } @{ $val->{build_requires} } };
+ }
+
+ return $meta;
+}
+
+1;
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
new file mode 100644
index 0000000..2889ece
--- /dev/null
+++ b/inc/Module/Install/RTx.pm
@@ -0,0 +1,316 @@
+#line 1
+package Module::Install::RTx;
+
+use 5.008;
+use strict;
+use warnings;
+no warnings 'once';
+
+use Term::ANSIColor qw(:constants);
+use Module::Install::Base;
+use base 'Module::Install::Base';
+our $VERSION = '0.43';
+
+use FindBin;
+use File::Glob ();
+use File::Basename ();
+
+my @DIRS = qw(etc lib html static bin sbin po var);
+my @INDEX_DIRS = qw(lib bin sbin);
+
+sub RTx {
+ my ( $self, $name, $extra_args ) = @_;
+ $extra_args ||= {};
+
+ # Set up names
+ my $fname = $name;
+ $fname =~ s!-!/!g;
+
+ $self->name( $name )
+ unless $self->name;
+ $self->all_from( "lib/$fname.pm" )
+ unless $self->version;
+ $self->abstract("$name Extension")
+ unless $self->abstract;
+ unless ( $extra_args->{no_readme_generation} ) {
+ $self->readme_from( "lib/$fname.pm",
+ { options => [ quotes => "none" ] } );
+ }
+ $self->add_metadata("x_module_install_rtx_version", $VERSION );
+
+ my $installdirs = $ENV{INSTALLDIRS};
+ for ( @ARGV ) {
+ if ( /INSTALLDIRS=(.*)/ ) {
+ $installdirs = $1;
+ }
+ }
+
+ # Try to find RT.pm
+ my @prefixes = qw( /opt /usr/local /home /usr /sw /usr/share/request-tracker4);
+ $ENV{RTHOME} =~ s{/RT\.pm$}{} if defined $ENV{RTHOME};
+ $ENV{RTHOME} =~ s{/lib/?$}{} if defined $ENV{RTHOME};
+ my @try = $ENV{RTHOME} ? ($ENV{RTHOME}, "$ENV{RTHOME}/lib") : ();
+ while (1) {
+ my @look = @INC;
+ unshift @look, grep {defined and -d $_} @try;
+ push @look, grep {defined and -d $_}
+ map { ( "$_/rt5/lib", "$_/lib/rt5", "$_/rt4/lib", "$_/lib/rt4", "$_/lib" ) } @prefixes;
+ last if eval {local @INC = @look; require RT; $RT::LocalLibPath};
+
+ warn
+ "Cannot find the location of RT.pm that defines \$RT::LocalPath in: @look\n";
+ my $given = $self->prompt("Path to directory containing your RT.pm:") or exit;
+ $given =~ s{/RT\.pm$}{};
+ $given =~ s{/lib/?$}{};
+ @try = ($given, "$given/lib");
+ }
+
+ print "Using RT configuration from $INC{'RT.pm'}:\n";
+
+ my $local_lib_path = $RT::LocalLibPath;
+ unshift @INC, $local_lib_path;
+ my $lib_path = File::Basename::dirname( $INC{'RT.pm'} );
+ unshift @INC, $lib_path;
+
+ # Set a baseline minimum version
+ unless ( $extra_args->{deprecated_rt} ) {
+ $self->requires_rt('4.0.0');
+ }
+
+ my $package = $name;
+ $package =~ s/-/::/g;
+ if ( $RT::CORED_PLUGINS{$package} ) {
+ my ($base_version) = $RT::VERSION =~ /(\d+\.\d+\.\d+)/;
+ die RED, <<"EOT";
+
+**** Error: Your installed version of RT ($RT::VERSION) already
+ contains this extension in core, so you don't need to
+ install it.
+
+ Check https://docs.bestpractical.com/rt/$base_version/RT_Config.html
+ to configure it.
+
+EOT
+ }
+
+ # Installation locations
+ my %path;
+ my $plugin_path;
+ if ( $installdirs && $installdirs eq 'vendor' ) {
+ $plugin_path = $RT::PluginPath;
+ } else {
+ $plugin_path = $RT::LocalPluginPath;
+ }
+ $path{$_} = $plugin_path . "/$name/$_"
+ foreach @DIRS;
+
+ # Copy RT 4.2.0 static files into NoAuth; insufficient for
+ # images, but good enough for css and js.
+ $path{static} = "$path{html}/NoAuth/"
+ unless $RT::StaticPath;
+
+ # Delete the ones we don't need
+ delete $path{$_} for grep {not -d "$FindBin::Bin/$_"} keys %path;
+
+ my %index = map { $_ => 1 } @INDEX_DIRS;
+ $self->no_index( directory => $_ ) foreach grep !$index{$_}, @DIRS;
+
+ my $args = join ', ', map "q($_)", map { ($_, "\$(DESTDIR)$path{$_}") }
+ sort keys %path;
+
+ printf "%-10s => %s\n", $_, $path{$_} for sort keys %path;
+
+ if ( my @dirs = map { ( -D => $_ ) } grep $path{$_}, qw(bin html sbin etc) ) {
+ my @po = map { ( -o => $_ ) }
+ grep -f,
+ File::Glob::bsd_glob("po/*.po");
+ $self->postamble(<< ".") if @po;
+lexicons ::
+\t\$(NOECHO) \$(PERL) -MLocale::Maketext::Extract::Run=xgettext -e \"xgettext(qw(@dirs @po))\"
+.
+ }
+
+ my $remove_files;
+ if( $extra_args->{'remove_files'} ){
+ $self->include('Module::Install::RTx::Remove');
+ our @remove_files;
+ eval { require "./etc/upgrade/remove_files" }
+ or print "No remove file located, no files to remove\n";
+ $remove_files = join ",", map {"q(\$(DESTDIR)$plugin_path/$name/$_)"} @remove_files;
+ }
+
+ $self->include('Module::Install::RTx::Runtime') if $self->admin;
+ $self->include_deps( 'YAML::Tiny', 0 ) if $self->admin;
+ my $postamble = << ".";
+install ::
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
+.
+
+ if( $remove_files ){
+ $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MModule::Install::RTx::Remove -e \"RTxRemove([$remove_files])\"
+.
+ }
+
+ $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MExtUtils::Install -e \"install({$args})\"
+.
+
+ if ( $path{var} and -d $RT::MasonDataDir ) {
+ my ( $uid, $gid ) = ( stat($RT::MasonDataDir) )[ 4, 5 ];
+ $postamble .= << ".";
+\t\$(NOECHO) chown -R $uid:$gid $path{var}
+.
+ }
+
+ my %has_etc;
+ if ( File::Glob::bsd_glob("$FindBin::Bin/etc/schema.*") ) {
+ $has_etc{schema}++;
+ }
+ if ( File::Glob::bsd_glob("$FindBin::Bin/etc/acl.*") ) {
+ $has_etc{acl}++;
+ }
+ if ( -e 'etc/initialdata' ) { $has_etc{initialdata}++; }
+ if ( grep { /\d+\.\d+\.\d+.*$/ } glob('etc/upgrade/*.*.*') ) {
+ $has_etc{upgrade}++;
+ }
+
+ $self->postamble("$postamble\n");
+ if ( $path{lib} ) {
+ $self->makemaker_args( INSTALLSITELIB => $path{'lib'} );
+ $self->makemaker_args( INSTALLARCHLIB => $path{'lib'} );
+ $self->makemaker_args( INSTALLVENDORLIB => $path{'lib'} )
+ } else {
+ $self->makemaker_args( PM => { "" => "" }, );
+ }
+
+ $self->makemaker_args( INSTALLSITEMAN1DIR => "$RT::LocalPath/man/man1" );
+ $self->makemaker_args( INSTALLSITEMAN3DIR => "$RT::LocalPath/man/man3" );
+ $self->makemaker_args( INSTALLSITEARCH => "$RT::LocalPath/man" );
+
+ # INSTALLDIRS=vendor should install manpages into /usr/share/man.
+ # That is the default path in most distributions. Need input from
+ # Redhat, Centos etc.
+ $self->makemaker_args( INSTALLVENDORMAN1DIR => "/usr/share/man/man1" );
+ $self->makemaker_args( INSTALLVENDORMAN3DIR => "/usr/share/man/man3" );
+ $self->makemaker_args( INSTALLVENDORARCH => "/usr/share/man" );
+
+ if (%has_etc) {
+ print "For first-time installation, type 'make initdb'.\n";
+ my $initdb = '';
+ $initdb .= <<"." if $has_etc{schema};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(schema \$(NAME) \$(VERSION)))"
+.
+ $initdb .= <<"." if $has_etc{acl};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(acl \$(NAME) \$(VERSION)))"
+.
+ $initdb .= <<"." if $has_etc{initialdata};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(insert \$(NAME) \$(VERSION)))"
+.
+ $self->postamble("initdb ::\n$initdb\n");
+ $self->postamble("initialize-database ::\n$initdb\n");
+ if ($has_etc{upgrade}) {
+ print "To upgrade from a previous version of this extension, use 'make upgrade-database'\n";
+ my $upgradedb = qq|\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(upgrade \$(NAME) \$(VERSION)))"\n|;
+ $self->postamble("upgrade-database ::\n$upgradedb\n");
+ $self->postamble("upgradedb ::\n$upgradedb\n");
+ }
+ }
+
+}
+
+sub requires_rt {
+ my ($self,$version) = @_;
+
+ _load_rt_handle();
+
+ if ($self->is_admin) {
+ $self->add_metadata("x_requires_rt", $version);
+ my @sorted = sort RT::Handle::cmp_version $version,'4.0.0';
+ $self->perl_version('5.008003') if $sorted[0] eq '4.0.0'
+ and (not $self->perl_version or '5.008003' > $self->perl_version);
+ @sorted = sort RT::Handle::cmp_version $version,'4.2.0';
+ $self->perl_version('5.010001') if $sorted[0] eq '4.2.0'
+ and (not $self->perl_version or '5.010001' > $self->perl_version);
+ }
+
+ # if we're exactly the same version as what we want, silently return
+ return if ($version eq $RT::VERSION);
+
+ my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+ if ($sorted[-1] eq $version) {
+ die RED, <<"EOT";
+
+**** Error: This extension requires RT $version. Your installed version
+ of RT ($RT::VERSION) is too old.
+
+EOT
+ }
+}
+
+sub requires_rt_plugin {
+ my $self = shift;
+ my ( $plugin ) = @_;
+
+ if ($self->is_admin) {
+ my $plugins = $self->Meta->{values}{"x_requires_rt_plugins"} || [];
+ push @{$plugins}, $plugin;
+ $self->add_metadata("x_requires_rt_plugins", $plugins);
+ }
+
+ my $path = $plugin;
+ $path =~ s{\:\:}{-}g;
+ $path = "$RT::LocalPluginPath/$path/lib";
+ if ( -e $path ) {
+ unshift @INC, $path;
+ } else {
+ my $name = $self->name;
+ my $msg = <<"EOT";
+
+**** Warning: $name requires that the $plugin plugin be installed and
+ enabled; it does not appear to be installed.
+EOT
+ warn RED, $msg, RESET, "\n";
+ }
+ $self->requires(@_);
+}
+
+sub rt_too_new {
+ my ($self,$version,$msg) = @_;
+ my $name = $self->name;
+ $msg ||= <<EOT;
+
+**** Warning: Your installed version of RT (%s) is too new; this extension
+ has not been tested on your version of RT and may not work as expected.
+EOT
+ $self->add_metadata("x_rt_too_new", $version) if $self->is_admin;
+
+ _load_rt_handle();
+ my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+ if ($sorted[0] eq $version) {
+ warn RED, sprintf($msg,$RT::VERSION), RESET, "\n";
+ }
+}
+
+# RT::Handle runs FinalizeDatabaseType which calls RT->Config->Get
+# On 3.8, this dies. On 4.0/4.2 ->Config transparently runs LoadConfig.
+# LoadConfig requires being able to read RT_SiteConfig.pm (root) so we'd
+# like to avoid pushing that on users.
+# Fake up just enough Config to let FinalizeDatabaseType finish, and
+# anyone later calling LoadConfig will overwrite our shenanigans.
+sub _load_rt_handle {
+ unless ($RT::Config) {
+ require RT::Config;
+ $RT::Config = RT::Config->new;
+ RT->Config->Set('DatabaseType','mysql');
+ }
+ require RT::Handle;
+}
+
+1;
+
+__END__
+
+#line 484
diff --git a/inc/Module/Install/RTx/Runtime.pm b/inc/Module/Install/RTx/Runtime.pm
new file mode 100644
index 0000000..ae07502
--- /dev/null
+++ b/inc/Module/Install/RTx/Runtime.pm
@@ -0,0 +1,80 @@
+#line 1
+package Module::Install::RTx::Runtime;
+
+use base 'Exporter';
+our @EXPORT = qw/RTxDatabase RTxPlugin/;
+
+use strict;
+use File::Basename ();
+
+sub _rt_runtime_load {
+ require RT;
+
+ eval { RT::LoadConfig(); };
+ if (my $err = $@) {
+ die $err unless $err =~ /^RT couldn't load RT config file/m;
+ my $warn = <<EOT;
+This usually means that your current user cannot read the file. You
+will likely need to run this installation step as root, or some user
+with more permissions.
+EOT
+ $err =~ s/This usually means.*/$warn/s;
+ die $err;
+ }
+}
+
+sub RTxDatabase {
+ my ($action, $name, $version) = @_;
+
+ _rt_runtime_load();
+
+ require RT::System;
+ my $has_upgrade = RT::System->can('AddUpgradeHistory');
+
+ my $lib_path = File::Basename::dirname($INC{'RT.pm'});
+ my @args = (
+ "-I.",
+ "-Ilib",
+ "-I$RT::LocalLibPath",
+ "-I$lib_path",
+ "$RT::SbinPath/rt-setup-database",
+ "--action" => $action,
+ ($action eq 'upgrade' ? () : ("--datadir" => "etc")),
+ (($action eq 'insert') ? ("--datafile" => "etc/initialdata") : ()),
+ "--dba" => $RT::DatabaseAdmin || $RT::DatabaseUser,
+ "--prompt-for-dba-password" => '',
+ ($has_upgrade ? ("--package" => $name, "--ext-version" => $version) : ()),
+ );
+ # If we're upgrading against an RT which isn't at least 4.2 (has
+ # AddUpgradeHistory) then pass --package. Upgrades against later RT
+ # releases will pick up --package from AddUpgradeHistory.
+ if ($action eq 'upgrade' and not $has_upgrade) {
+ push @args, "--package" => $name;
+ }
+
+ print "$^X @args\n";
+ (system($^X, @args) == 0) or die "...returned with error: $?\n";
+}
+
+sub RTxPlugin {
+ my ($name) = @_;
+
+ _rt_runtime_load();
+ require YAML::Tiny;
+ my $data = YAML::Tiny::LoadFile('META.yml');
+ my $name = $data->{name};
+
+ my @enabled = RT->Config->Get('Plugins');
+ for my $required (@{$data->{x_requires_rt_plugins} || []}) {
+ next if grep {$required eq $_} @enabled;
+
+ warn <<"EOT";
+
+**** Warning: $name requires that the $required plugin be installed and
+ enabled; it is not currently in \@Plugins.
+
+EOT
+ }
+}
+
+1;
diff --git a/inc/Module/Install/ReadmeFromPod.pm b/inc/Module/Install/ReadmeFromPod.pm
new file mode 100644
index 0000000..3738232
--- /dev/null
+++ b/inc/Module/Install/ReadmeFromPod.pm
@@ -0,0 +1,184 @@
+#line 1
+package Module::Install::ReadmeFromPod;
+
+use 5.006;
+use strict;
+use warnings;
+use base qw(Module::Install::Base);
+use vars qw($VERSION);
+
+$VERSION = '0.30';
+
+{
+
+ # these aren't defined until after _require_admin is run, so
+ # define them so prototypes are available during compilation.
+ sub io;
+ sub capture(&;@);
+
+#line 28
+
+ my $done = 0;
+
+ sub _require_admin {
+
+ # do this once to avoid redefinition warnings from IO::All
+ return if $done;
+
+ require IO::All;
+ IO::All->import( '-binary' );
+
+ require Capture::Tiny;
+ Capture::Tiny->import ( 'capture' );
+
+ return;
+ }
+
+}
+
+sub readme_from {
+ my $self = shift;
+ return unless $self->is_admin;
+
+ _require_admin;
+
+ # Input file
+ my $in_file = shift || $self->_all_from
+ or die "Can't determine file to make readme_from";
+
+ # Get optional arguments
+ my ($clean, $format, $out_file, $options);
+ my $args = shift;
+ if ( ref $args ) {
+ # Arguments are in a hashref
+ if ( ref($args) ne 'HASH' ) {
+ die "Expected a hashref but got a ".ref($args)."\n";
+ } else {
+ $clean = $args->{'clean'};
+ $format = $args->{'format'};
+ $out_file = $args->{'output_file'};
+ $options = $args->{'options'};
+ }
+ } else {
+ # Arguments are in a list
+ $clean = $args;
+ $format = shift;
+ $out_file = shift;
+ $options = \@_;
+ }
+
+ # Default values;
+ $clean ||= 0;
+ $format ||= 'txt';
+
+ # Generate README
+ print "readme_from $in_file to $format\n";
+ if ($format =~ m/te?xt/) {
+ $out_file = $self->_readme_txt($in_file, $out_file, $options);
+ } elsif ($format =~ m/html?/) {
+ $out_file = $self->_readme_htm($in_file, $out_file, $options);
+ } elsif ($format eq 'man') {
+ $out_file = $self->_readme_man($in_file, $out_file, $options);
+ } elsif ($format eq 'md') {
+ $out_file = $self->_readme_md($in_file, $out_file, $options);
+ } elsif ($format eq 'pdf') {
+ $out_file = $self->_readme_pdf($in_file, $out_file, $options);
+ }
+
+ if ($clean) {
+ $self->clean_files($out_file);
+ }
+
+ return 1;
+}
+
+
+sub _readme_txt {
+ my ($self, $in_file, $out_file, $options) = @_;
+ $out_file ||= 'README';
+ require Pod::Text;
+ my $parser = Pod::Text->new( @$options );
+ my $io = io->file($out_file)->open(">");
+ my $out_fh = $io->io_handle;
+ $parser->output_fh( *$out_fh );
+ $parser->parse_file( $in_file );
+ return $out_file;
+}
+
+
+sub _readme_htm {
+ my ($self, $in_file, $out_file, $options) = @_;
+ $out_file ||= 'README.htm';
+ require Pod::Html;
+ my ($o) = capture {
+ Pod::Html::pod2html(
+ "--infile=$in_file",
+ "--outfile=-",
+ @$options,
+ );
+ };
+ io->file($out_file)->print($o);
+ # Remove temporary files if needed
+ for my $file ('pod2htmd.tmp', 'pod2htmi.tmp') {
+ if (-e $file) {
+ unlink $file or warn "Warning: Could not remove file '$file'.\n$!\n";
+ }
+ }
+ return $out_file;
+}
+
+
+sub _readme_man {
+ my ($self, $in_file, $out_file, $options) = @_;
+ $out_file ||= 'README.1';
+ require Pod::Man;
+ my $parser = Pod::Man->new( @$options );
+ my $io = io->file($out_file)->open(">");
+ my $out_fh = $io->io_handle;
+ $parser->output_fh( *$out_fh );
+ $parser->parse_file( $in_file );
+ return $out_file;
+}
+
+
+sub _readme_pdf {
+ my ($self, $in_file, $out_file, $options) = @_;
+ $out_file ||= 'README.pdf';
+ eval { require App::pod2pdf; }
+ or die "Could not generate $out_file because pod2pdf could not be found\n";
+ my $parser = App::pod2pdf->new( @$options );
+ $parser->parse_from_file($in_file);
+ my ($o) = capture { $parser->output };
+ io->file($out_file)->print($o);
+ return $out_file;
+}
+
+sub _readme_md {
+ my ($self, $in_file, $out_file, $options) = @_;
+ $out_file ||= 'README.md';
+ require Pod::Markdown;
+ my $parser = Pod::Markdown->new( @$options );
+ my $io = io->file($out_file)->open(">");
+ my $out_fh = $io->io_handle;
+ $parser->output_fh( *$out_fh );
+ $parser->parse_file( $in_file );
+ return $out_file;
+}
+
+
+sub _all_from {
+ my $self = shift;
+ return unless $self->admin->{extensions};
+ my ($metadata) = grep {
+ ref($_) eq 'Module::Install::Metadata';
+ } @{$self->admin->{extensions}};
+ return unless $metadata;
+ return $metadata->{values}{all_from} || '';
+}
+
+'Readme!';
+
+__END__
+
+#line 316
+
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
new file mode 100644
index 0000000..f7aa615
--- /dev/null
+++ b/inc/Module/Install/Win32.pm
@@ -0,0 +1,64 @@
+#line 1
+package Module::Install::Win32;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = 'Module::Install::Base';
+ $ISCORE = 1;
+}
+
+# determine if the user needs nmake, and download it if needed
+sub check_nmake {
+ my $self = shift;
+ $self->load('can_run');
+ $self->load('get_file');
+
+ require Config;
+ return unless (
+ $^O eq 'MSWin32' and
+ $Config::Config{make} and
+ $Config::Config{make} =~ /^nmake\b/i and
+ ! $self->can_run('nmake')
+ );
+
+ print "The required 'nmake' executable not found, fetching it...\n";
+
+ require File::Basename;
+ my $rv = $self->get_file(
+ url => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe',
+ ftp_url => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe',
+ local_dir => File::Basename::dirname($^X),
+ size => 51928,
+ run => 'Nmake15.exe /o > nul',
+ check_for => 'Nmake.exe',
+ remove => 1,
+ );
+
+ die <<'END_MESSAGE' unless $rv;
+
+-------------------------------------------------------------------------------
+
+Since you are using Microsoft Windows, you will need the 'nmake' utility
+before installation. It's available at:
+
+ http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe
+ or
+ ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe
+
+Please download the file manually, save it to a directory in %PATH% (e.g.
+C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to
+that directory, and run "Nmake15.exe" from there; that will create the
+'nmake.exe' file needed by this module.
+
+You may then resume the installation process described in README.
+
+-------------------------------------------------------------------------------
+END_MESSAGE
+
+}
+
+1;
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
new file mode 100644
index 0000000..2db861a
--- /dev/null
+++ b/inc/Module/Install/WriteAll.pm
@@ -0,0 +1,63 @@
+#line 1
+package Module::Install::WriteAll;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+ $VERSION = '1.19';
+ @ISA = qw{Module::Install::Base};
+ $ISCORE = 1;
+}
+
+sub WriteAll {
+ my $self = shift;
+ my %args = (
+ meta => 1,
+ sign => 0,
+ inline => 0,
+ check_nmake => 1,
+ @_,
+ );
+
+ $self->sign(1) if $args{sign};
+ $self->admin->WriteAll(%args) if $self->is_admin;
+
+ $self->check_nmake if $args{check_nmake};
+ unless ( $self->makemaker_args->{PL_FILES} ) {
+ # XXX: This still may be a bit over-defensive...
+ unless ($self->makemaker(6.25)) {
+ $self->makemaker_args( PL_FILES => {} ) if -f 'Build.PL';
+ }
+ }
+
+ # Until ExtUtils::MakeMaker support MYMETA.yml, make sure
+ # we clean it up properly ourself.
+ $self->realclean_files('MYMETA.yml');
+
+ if ( $args{inline} ) {
+ $self->Inline->write;
+ } else {
+ $self->Makefile->write;
+ }
+
+ # The Makefile write process adds a couple of dependencies,
+ # so write the META.yml files after the Makefile.
+ if ( $args{meta} ) {
+ $self->Meta->write;
+ }
+
+ # Experimental support for MYMETA
+ if ( $ENV{X_MYMETA} ) {
+ if ( $ENV{X_MYMETA} eq 'JSON' ) {
+ $self->Meta->write_mymeta_json;
+ } else {
+ $self->Meta->write_mymeta_yaml;
+ }
+ }
+
+ return 1;
+}
+
+1;
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
new file mode 100644
index 0000000..fb157a6
--- /dev/null
+++ b/inc/YAML/Tiny.pm
@@ -0,0 +1,872 @@
+#line 1
+use 5.008001; # sane UTF-8 support
+use strict;
+use warnings;
+package YAML::Tiny; # git description: v1.72-7-g8682f63
+# XXX-INGY is 5.8.1 too old/broken for utf8?
+# XXX-XDG Lancaster consensus was that it was sufficient until
+# proven otherwise
+
+our $VERSION = '1.73';
+
+#####################################################################
+# The YAML::Tiny API.
+#
+# These are the currently documented API functions/methods and
+# exports:
+
+use Exporter;
+our @ISA = qw{ Exporter };
+our @EXPORT = qw{ Load Dump };
+our @EXPORT_OK = qw{ LoadFile DumpFile freeze thaw };
+
+###
+# Functional/Export API:
+
+sub Dump {
+ return YAML::Tiny->new(@_)->_dump_string;
+}
+
+# XXX-INGY Returning last document seems a bad behavior.
+# XXX-XDG I think first would seem more natural, but I don't know
+# that it's worth changing now
+sub Load {
+ my $self = YAML::Tiny->_load_string(@_);
+ if ( wantarray ) {
+ return @$self;
+ } else {
+ # To match YAML.pm, return the last document
+ return $self->[-1];
+ }
+}
+
+# XXX-INGY Do we really need freeze and thaw?
+# XXX-XDG I don't think so. I'd support deprecating them.
+BEGIN {
+ *freeze = \&Dump;
+ *thaw = \&Load;
+}
+
+sub DumpFile {
+ my $file = shift;
+ return YAML::Tiny->new(@_)->_dump_file($file);
+}
+
+sub LoadFile {
+ my $file = shift;
+ my $self = YAML::Tiny->_load_file($file);
+ if ( wantarray ) {
+ return @$self;
+ } else {
+ # Return only the last document to match YAML.pm,
+ return $self->[-1];
+ }
+}
+
+
+###
+# Object Oriented API:
+
+# Create an empty YAML::Tiny object
+# XXX-INGY Why do we use ARRAY object?
+# NOTE: I get it now, but I think it's confusing and not needed.
+# Will change it on a branch later, for review.
+#
+# XXX-XDG I don't support changing it yet. It's a very well-documented
+# "API" of YAML::Tiny. I'd support deprecating it, but Adam suggested
+# we not change it until YAML.pm's own OO API is established so that
+# users only have one API change to digest, not two
+sub new {
+ my $class = shift;
+ bless [ @_ ], $class;
+}
+
+# XXX-INGY It probably doesn't matter, and it's probably too late to
+# change, but 'read/write' are the wrong names. Read and Write
+# are actions that take data from storage to memory
+# characters/strings. These take the data to/from storage to native
+# Perl objects, which the terms dump and load are meant. As long as
+# this is a legacy quirk to YAML::Tiny it's ok, but I'd prefer not
+# to add new {read,write}_* methods to this API.
+
+sub read_string {
+ my $self = shift;
+ $self->_load_string(@_);
+}
+
+sub write_string {
+ my $self = shift;
+ $self->_dump_string(@_);
+}
+
+sub read {
+ my $self = shift;
+ $self->_load_file(@_);
+}
+
+sub write {
+ my $self = shift;
+ $self->_dump_file(@_);
+}
+
+
+
+
+#####################################################################
+# Constants
+
+# Printed form of the unprintable characters in the lowest range
+# of ASCII characters, listed by ASCII ordinal position.
+my @UNPRINTABLE = qw(
+ 0 x01 x02 x03 x04 x05 x06 a
+ b t n v f r x0E x0F
+ x10 x11 x12 x13 x14 x15 x16 x17
+ x18 x19 x1A e x1C x1D x1E x1F
+);
+
+# Printable characters for escapes
+my %UNESCAPES = (
+ 0 => "\x00", z => "\x00", N => "\x85",
+ a => "\x07", b => "\x08", t => "\x09",
+ n => "\x0a", v => "\x0b", f => "\x0c",
+ r => "\x0d", e => "\x1b", '\\' => '\\',
+);
+
+# XXX-INGY
+# I(ngy) need to decide if these values should be quoted in
+# YAML::Tiny or not. Probably yes.
+
+# These 3 values have special meaning when unquoted and using the
+# default YAML schema. They need quotes if they are strings.
+my %QUOTE = map { $_ => 1 } qw{
+ null true false
+};
+
+# The commented out form is simpler, but overloaded the Perl regex
+# engine due to recursion and backtracking problems on strings
+# larger than 32,000ish characters. Keep it for reference purposes.
+# qr/\"((?:\\.|[^\"])*)\"/
+my $re_capture_double_quoted = qr/\"([^\\"]*(?:\\.[^\\"]*)*)\"/;
+my $re_capture_single_quoted = qr/\'([^\']*(?:\'\'[^\']*)*)\'/;
+# unquoted re gets trailing space that needs to be stripped
+my $re_capture_unquoted_key = qr/([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*\:(?:\s+|$))/;
+my $re_trailing_comment = qr/(?:\s+\#.*)?/;
+my $re_key_value_separator = qr/\s*:(?:\s+(?:\#.*)?|$)/;
+
+
+
+
+
+#####################################################################
+# YAML::Tiny Implementation.
+#
+# These are the private methods that do all the work. They may change
+# at any time.
+
+
+###
+# Loader functions:
+
+# Create an object from a file
+sub _load_file {
+ my $class = ref $_[0] ? ref shift : shift;
+
+ # Check the file
+ my $file = shift or $class->_error( 'You did not specify a file name' );
+ $class->_error( "File '$file' does not exist" )
+ unless -e $file;
+ $class->_error( "'$file' is a directory, not a file" )
+ unless -f _;
+ $class->_error( "Insufficient permissions to read '$file'" )
+ unless -r _;
+
+ # Open unbuffered with strict UTF-8 decoding and no translation layers
+ open( my $fh, "<:unix:encoding(UTF-8)", $file );
+ unless ( $fh ) {
+ $class->_error("Failed to open file '$file': $!");
+ }
+
+ # flock if available (or warn if not possible for OS-specific reasons)
+ if ( _can_flock() ) {
+ flock( $fh, Fcntl::LOCK_SH() )
+ or warn "Couldn't lock '$file' for reading: $!";
+ }
+
+ # slurp the contents
+ my $contents = eval {
+ use warnings FATAL => 'utf8';
+ local $/;
+ <$fh>
+ };
+ if ( my $err = $@ ) {
+ $class->_error("Error reading from file '$file': $err");
+ }
+
+ # close the file (release the lock)
+ unless ( close $fh ) {
+ $class->_error("Failed to close file '$file': $!");
+ }
+
+ $class->_load_string( $contents );
+}
+
+# Create an object from a string
+sub _load_string {
+ my $class = ref $_[0] ? ref shift : shift;
+ my $self = bless [], $class;
+ my $string = $_[0];
+ eval {
+ unless ( defined $string ) {
+ die \"Did not provide a string to load";
+ }
+
+ # Check if Perl has it marked as characters, but it's internally
+ # inconsistent. E.g. maybe latin1 got read on a :utf8 layer
+ if ( utf8::is_utf8($string) && ! utf8::valid($string) ) {
+ die \<<'...';
+Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set).
+Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
+...
+ }
+
+ # Ensure Unicode character semantics, even for 0x80-0xff
+ utf8::upgrade($string);
+
+ # Check for and strip any leading UTF-8 BOM
+ $string =~ s/^\x{FEFF}//;
+
+ # Check for some special cases
+ return $self unless length $string;
+
+ # Split the file into lines
+ my @lines = grep { ! /^\s*(?:\#.*)?\z/ }
+ split /(?:\015{1,2}\012|\015|\012)/, $string;
+
+ # Strip the initial YAML header
+ @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines;
+
+ # A nibbling parser
+ my $in_document = 0;
+ while ( @lines ) {
+ # Do we have a document header?
+ if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) {
+ # Handle scalar documents
+ shift @lines;
+ if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) {
+ push @$self,
+ $self->_load_scalar( "$1", [ undef ], \@lines );
+ next;
+ }
+ $in_document = 1;
+ }
+
+ if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) {
+ # A naked document
+ push @$self, undef;
+ while ( @lines and $lines[0] !~ /^---/ ) {
+ shift @lines;
+ }
+ $in_document = 0;
+
+ # XXX The final '-+$' is to look for -- which ends up being an
+ # error later.
+ } elsif ( ! $in_document && @$self ) {
+ # only the first document can be explicit
+ die \"YAML::Tiny failed to classify the line '$lines[0]'";
+ } elsif ( $lines[0] =~ /^\s*\-(?:\s|$|-+$)/ ) {
+ # An array at the root
+ my $document = [ ];
+ push @$self, $document;
+ $self->_load_array( $document, [ 0 ], \@lines );
+
+ } elsif ( $lines[0] =~ /^(\s*)\S/ ) {
+ # A hash at the root
+ my $document = { };
+ push @$self, $document;
+ $self->_load_hash( $document, [ length($1) ], \@lines );
+
+ } else {
+ # Shouldn't get here. @lines have whitespace-only lines
+ # stripped, and previous match is a line with any
+ # non-whitespace. So this clause should only be reachable via
+ # a perlbug where \s is not symmetric with \S
+
+ # uncoverable statement
+ die \"YAML::Tiny failed to classify the line '$lines[0]'";
+ }
+ }
+ };
+ my $err = $@;
+ if ( ref $err eq 'SCALAR' ) {
+ $self->_error(${$err});
+ } elsif ( $err ) {
+ $self->_error($err);
+ }
+
+ return $self;
+}
+
+sub _unquote_single {
+ my ($self, $string) = @_;
+ return '' unless length $string;
+ $string =~ s/\'\'/\'/g;
+ return $string;
+}
+
+sub _unquote_double {
+ my ($self, $string) = @_;
+ return '' unless length $string;
+ $string =~ s/\\"/"/g;
+ $string =~
+ s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))}
+ {(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}}gex;
+ return $string;
+}
+
+# Load a YAML scalar string to the actual Perl scalar
+sub _load_scalar {
+ my ($self, $string, $indent, $lines) = @_;
+
+ # Trim trailing whitespace
+ $string =~ s/\s*\z//;
+
+ # Explitic null/undef
+ return undef if $string eq '~';
+
+ # Single quote
+ if ( $string =~ /^$re_capture_single_quoted$re_trailing_comment\z/ ) {
+ return $self->_unquote_single($1);
+ }
+
+ # Double quote.
+ if ( $string =~ /^$re_capture_double_quoted$re_trailing_comment\z/ ) {
+ return $self->_unquote_double($1);
+ }
+
+ # Special cases
+ if ( $string =~ /^[\'\"!&]/ ) {
+ die \"YAML::Tiny does not support a feature in line '$string'";
+ }
+ return {} if $string =~ /^{}(?:\s+\#.*)?\z/;
+ return [] if $string =~ /^\[\](?:\s+\#.*)?\z/;
+
+ # Regular unquoted string
+ if ( $string !~ /^[>|]/ ) {
+ die \"YAML::Tiny found illegal characters in plain scalar: '$string'"
+ if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or
+ $string =~ /:(?:\s|$)/;
+ $string =~ s/\s+#.*\z//;
+ return $string;
+ }
+
+ # Error
+ die \"YAML::Tiny failed to find multi-line scalar content" unless @$lines;
+
+ # Check the indent depth
+ $lines->[0] =~ /^(\s*)/;
+ $indent->[-1] = length("$1");
+ if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) {
+ die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+ }
+
+ # Pull the lines
+ my @multiline = ();
+ while ( @$lines ) {
+ $lines->[0] =~ /^(\s*)/;
+ last unless length($1) >= $indent->[-1];
+ push @multiline, substr(shift(@$lines), $indent->[-1]);
+ }
+
+ my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n";
+ my $t = (substr($string, 1, 1) eq '-') ? '' : "\n";
+ return join( $j, @multiline ) . $t;
+}
+
+# Load an array
+sub _load_array {
+ my ($self, $array, $indent, $lines) = @_;
+
+ while ( @$lines ) {
+ # Check for a new document
+ if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
+ while ( @$lines and $lines->[0] !~ /^---/ ) {
+ shift @$lines;
+ }
+ return 1;
+ }
+
+ # Check the indent level
+ $lines->[0] =~ /^(\s*)/;
+ if ( length($1) < $indent->[-1] ) {
+ return 1;
+ } elsif ( length($1) > $indent->[-1] ) {
+ die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+ }
+
+ if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) {
+ # Inline nested hash
+ my $indent2 = length("$1");
+ $lines->[0] =~ s/-/ /;
+ push @$array, { };
+ $self->_load_hash( $array->[-1], [ @$indent, $indent2 ], $lines );
+
+ } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) {
+ shift @$lines;
+ unless ( @$lines ) {
+ push @$array, undef;
+ return 1;
+ }
+ if ( $lines->[0] =~ /^(\s*)\-/ ) {
+ my $indent2 = length("$1");
+ if ( $indent->[-1] == $indent2 ) {
+ # Null array entry
+ push @$array, undef;
+ } else {
+ # Naked indenter
+ push @$array, [ ];
+ $self->_load_array(
+ $array->[-1], [ @$indent, $indent2 ], $lines
+ );
+ }
+
+ } elsif ( $lines->[0] =~ /^(\s*)\S/ ) {
+ push @$array, { };
+ $self->_load_hash(
+ $array->[-1], [ @$indent, length("$1") ], $lines
+ );
+
+ } else {
+ die \"YAML::Tiny failed to classify line '$lines->[0]'";
+ }
+
+ } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) {
+ # Array entry with a value
+ shift @$lines;
+ push @$array, $self->_load_scalar(
+ "$2", [ @$indent, undef ], $lines
+ );
+
+ } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) {
+ # This is probably a structure like the following...
+ # ---
+ # foo:
+ # - list
+ # bar: value
+ #
+ # ... so lets return and let the hash parser handle it
+ return 1;
+
+ } else {
+ die \"YAML::Tiny failed to classify line '$lines->[0]'";
+ }
+ }
+
+ return 1;
+}
+
+# Load a hash
+sub _load_hash {
+ my ($self, $hash, $indent, $lines) = @_;
+
+ while ( @$lines ) {
+ # Check for a new document
+ if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
+ while ( @$lines and $lines->[0] !~ /^---/ ) {
+ shift @$lines;
+ }
+ return 1;
+ }
+
+ # Check the indent level
+ $lines->[0] =~ /^(\s*)/;
+ if ( length($1) < $indent->[-1] ) {
+ return 1;
+ } elsif ( length($1) > $indent->[-1] ) {
+ die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+ }
+
+ # Find the key
+ my $key;
+
+ # Quoted keys
+ if ( $lines->[0] =~
+ s/^\s*$re_capture_single_quoted$re_key_value_separator//
+ ) {
+ $key = $self->_unquote_single($1);
+ }
+ elsif ( $lines->[0] =~
+ s/^\s*$re_capture_double_quoted$re_key_value_separator//
+ ) {
+ $key = $self->_unquote_double($1);
+ }
+ elsif ( $lines->[0] =~
+ s/^\s*$re_capture_unquoted_key$re_key_value_separator//
+ ) {
+ $key = $1;
+ $key =~ s/\s+$//;
+ }
+ elsif ( $lines->[0] =~ /^\s*\?/ ) {
+ die \"YAML::Tiny does not support a feature in line '$lines->[0]'";
+ }
+ else {
+ die \"YAML::Tiny failed to classify line '$lines->[0]'";
+ }
+
+ if ( exists $hash->{$key} ) {
+ warn "YAML::Tiny found a duplicate key '$key' in line '$lines->[0]'";
+ }
+
+ # Do we have a value?
+ if ( length $lines->[0] ) {
+ # Yes
+ $hash->{$key} = $self->_load_scalar(
+ shift(@$lines), [ @$indent, undef ], $lines
+ );
+ } else {
+ # An indent
+ shift @$lines;
+ unless ( @$lines ) {
+ $hash->{$key} = undef;
+ return 1;
+ }
+ if ( $lines->[0] =~ /^(\s*)-/ ) {
+ $hash->{$key} = [];
+ $self->_load_array(
+ $hash->{$key}, [ @$indent, length($1) ], $lines
+ );
+ } elsif ( $lines->[0] =~ /^(\s*)./ ) {
+ my $indent2 = length("$1");
+ if ( $indent->[-1] >= $indent2 ) {
+ # Null hash entry
+ $hash->{$key} = undef;
+ } else {
+ $hash->{$key} = {};
+ $self->_load_hash(
+ $hash->{$key}, [ @$indent, length($1) ], $lines
+ );
+ }
+ }
+ }
+ }
+
+ return 1;
+}
+
+
+###
+# Dumper functions:
+
+# Save an object to a file
+sub _dump_file {
+ my $self = shift;
+
+ require Fcntl;
+
+ # Check the file
+ my $file = shift or $self->_error( 'You did not specify a file name' );
+
+ my $fh;
+ # flock if available (or warn if not possible for OS-specific reasons)
+ if ( _can_flock() ) {
+ # Open without truncation (truncate comes after lock)
+ my $flags = Fcntl::O_WRONLY()|Fcntl::O_CREAT();
+ sysopen( $fh, $file, $flags )
+ or $self->_error("Failed to open file '$file' for writing: $!");
+
+ # Use no translation and strict UTF-8
+ binmode( $fh, ":raw:encoding(UTF-8)");
+
+ flock( $fh, Fcntl::LOCK_EX() )
+ or warn "Couldn't lock '$file' for reading: $!";
+
+ # truncate and spew contents
+ truncate $fh, 0;
+ seek $fh, 0, 0;
+ }
+ else {
+ open $fh, ">:unix:encoding(UTF-8)", $file;
+ }
+
+ # serialize and spew to the handle
+ print {$fh} $self->_dump_string;
+
+ # close the file (release the lock)
+ unless ( close $fh ) {
+ $self->_error("Failed to close file '$file': $!");
+ }
+
+ return 1;
+}
+
+# Save an object to a string
+sub _dump_string {
+ my $self = shift;
+ return '' unless ref $self && @$self;
+
+ # Iterate over the documents
+ my $indent = 0;
+ my @lines = ();
+
+ eval {
+ foreach my $cursor ( @$self ) {
+ push @lines, '---';
+
+ # An empty document
+ if ( ! defined $cursor ) {
+ # Do nothing
+
+ # A scalar document
+ } elsif ( ! ref $cursor ) {
+ $lines[-1] .= ' ' . $self->_dump_scalar( $cursor );
+
+ # A list at the root
+ } elsif ( ref $cursor eq 'ARRAY' ) {
+ unless ( @$cursor ) {
+ $lines[-1] .= ' []';
+ next;
+ }
+ push @lines, $self->_dump_array( $cursor, $indent, {} );
+
+ # A hash at the root
+ } elsif ( ref $cursor eq 'HASH' ) {
+ unless ( %$cursor ) {
+ $lines[-1] .= ' {}';
+ next;
+ }
+ push @lines, $self->_dump_hash( $cursor, $indent, {} );
+
+ } else {
+ die \("Cannot serialize " . ref($cursor));
+ }
+ }
+ };
+ if ( ref $@ eq 'SCALAR' ) {
+ $self->_error(${$@});
+ } elsif ( $@ ) {
+ $self->_error($@);
+ }
+
+ join '', map { "$_\n" } @lines;
+}
+
+sub _has_internal_string_value {
+ my $value = shift;
+ my $b_obj = B::svref_2object(\$value); # for round trip problem
+ return $b_obj->FLAGS & B::SVf_POK();
+}
+
+sub _dump_scalar {
+ my $string = $_[1];
+ my $is_key = $_[2];
+ # Check this before checking length or it winds up looking like a string!
+ my $has_string_flag = _has_internal_string_value($string);
+ return '~' unless defined $string;
+ return "''" unless length $string;
+ if (Scalar::Util::looks_like_number($string)) {
+ # keys and values that have been used as strings get quoted
+ if ( $is_key || $has_string_flag ) {
+ return qq['$string'];
+ }
+ else {
+ return $string;
+ }
+ }
+ if ( $string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/ ) {
+ $string =~ s/\\/\\\\/g;
+ $string =~ s/"/\\"/g;
+ $string =~ s/\n/\\n/g;
+ $string =~ s/[\x85]/\\N/g;
+ $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g;
+ $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge;
+ return qq|"$string"|;
+ }
+ if ( $string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or
+ $QUOTE{$string}
+ ) {
+ return "'$string'";
+ }
+ return $string;
+}
+
+sub _dump_array {
+ my ($self, $array, $indent, $seen) = @_;
+ if ( $seen->{refaddr($array)}++ ) {
+ die \"YAML::Tiny does not support circular references";
+ }
+ my @lines = ();
+ foreach my $el ( @$array ) {
+ my $line = (' ' x $indent) . '-';
+ my $type = ref $el;
+ if ( ! $type ) {
+ $line .= ' ' . $self->_dump_scalar( $el );
+ push @lines, $line;
+
+ } elsif ( $type eq 'ARRAY' ) {
+ if ( @$el ) {
+ push @lines, $line;
+ push @lines, $self->_dump_array( $el, $indent + 1, $seen );
+ } else {
+ $line .= ' []';
+ push @lines, $line;
+ }
+
+ } elsif ( $type eq 'HASH' ) {
+ if ( keys %$el ) {
+ push @lines, $line;
+ push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
+ } else {
+ $line .= ' {}';
+ push @lines, $line;
+ }
+
+ } else {
+ die \"YAML::Tiny does not support $type references";
+ }
+ }
+
+ @lines;
+}
+
+sub _dump_hash {
+ my ($self, $hash, $indent, $seen) = @_;
+ if ( $seen->{refaddr($hash)}++ ) {
+ die \"YAML::Tiny does not support circular references";
+ }
+ my @lines = ();
+ foreach my $name ( sort keys %$hash ) {
+ my $el = $hash->{$name};
+ my $line = (' ' x $indent) . $self->_dump_scalar($name, 1) . ":";
+ my $type = ref $el;
+ if ( ! $type ) {
+ $line .= ' ' . $self->_dump_scalar( $el );
+ push @lines, $line;
+
+ } elsif ( $type eq 'ARRAY' ) {
+ if ( @$el ) {
+ push @lines, $line;
+ push @lines, $self->_dump_array( $el, $indent + 1, $seen );
+ } else {
+ $line .= ' []';
+ push @lines, $line;
+ }
+
+ } elsif ( $type eq 'HASH' ) {
+ if ( keys %$el ) {
+ push @lines, $line;
+ push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
+ } else {
+ $line .= ' {}';
+ push @lines, $line;
+ }
+
+ } else {
+ die \"YAML::Tiny does not support $type references";
+ }
+ }
+
+ @lines;
+}
+
+
+
+#####################################################################
+# DEPRECATED API methods:
+
+# Error storage (DEPRECATED as of 1.57)
+our $errstr = '';
+
+# Set error
+sub _error {
+ require Carp;
+ $errstr = $_[1];
+ $errstr =~ s/ at \S+ line \d+.*//;
+ Carp::croak( $errstr );
+}
+
+# Retrieve error
+my $errstr_warned;
+sub errstr {
+ require Carp;
+ Carp::carp( "YAML::Tiny->errstr and \$YAML::Tiny::errstr is deprecated" )
+ unless $errstr_warned++;
+ $errstr;
+}
+
+
+
+
+#####################################################################
+# Helper functions. Possibly not needed.
+
+
+# Use to detect nv or iv
+use B;
+
+# XXX-INGY Is flock YAML::Tiny's responsibility?
+# Some platforms can't flock :-(
+# XXX-XDG I think it is. When reading and writing files, we ought
+# to be locking whenever possible. People (foolishly) use YAML
+# files for things like session storage, which has race issues.
+my $HAS_FLOCK;
+sub _can_flock {
+ if ( defined $HAS_FLOCK ) {
+ return $HAS_FLOCK;
+ }
+ else {
+ require Config;
+ my $c = \%Config::Config;
+ $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/;
+ require Fcntl if $HAS_FLOCK;
+ return $HAS_FLOCK;
+ }
+}
+
+
+# XXX-INGY Is this core in 5.8.1? Can we remove this?
+# XXX-XDG Scalar::Util 1.18 didn't land until 5.8.8, so we need this
+#####################################################################
+# Use Scalar::Util if possible, otherwise emulate it
+
+use Scalar::Util ();
+BEGIN {
+ local $@;
+ if ( eval { Scalar::Util->VERSION(1.18); } ) {
+ *refaddr = *Scalar::Util::refaddr;
+ }
+ else {
+ eval <<'END_PERL';
+# Scalar::Util failed to load or too old
+sub refaddr {
+ my $pkg = ref($_[0]) or return undef;
+ if ( !! UNIVERSAL::can($_[0], 'can') ) {
+ bless $_[0], 'Scalar::Util::Fake';
+ } else {
+ $pkg = undef;
+ }
+ "$_[0]" =~ /0x(\w+)/;
+ my $i = do { no warnings 'portable'; hex $1 };
+ bless $_[0], $pkg if defined $pkg;
+ $i;
+}
+END_PERL
+ }
+}
+
+delete $YAML::Tiny::{refaddr};
+
+1;
+
+# XXX-INGY Doc notes I'm putting up here. Changing the doc when it's wrong
+# but leaving grey area stuff up here.
+#
+# I would like to change Read/Write to Load/Dump below without
+# changing the actual API names.
+#
+# It might be better to put Load/Dump API in the SYNOPSIS instead of the
+# dubious OO API.
+#
+# null and bool explanations may be outdated.
+
+__END__
+
+#line 1487
-----------------------------------------------------------------------
hooks/post-receive
--
rt-extension-turbo
More information about the Bps-public-commit
mailing list