Now it works on the phone (was missing a quote), newer desktop anki 25.07.5 (_page broke), and with the last function I can now return to a local card after doing a remote one without it getting stuck, so I can mix different games in the same review session (mw.reviewer.web.setUrl()
then mw.moveToState("review")
). Also checkercruncher requires me to be logged in before I can move with one tap, and that applies on the second card because I didn’t get the timing right yet.
This is how I have it as-is, less user cookies, without cleaning up dead code/comments:
iframe2open_link_internally/__init__.py anki addon
from aqt import gui_hooks, mw
from aqt.qt import QUrl
import aqt#.reviewer.Reviewer
from aqt.qt import QWebEngineScript
import _thread, time
iframe2open_link_internally_oldurl = QUrl("unset")
def reviewer_did_show_question_open_link_internally(card):
if "iframe2internal::front" in card._note.tags:
open_link_internally(card)
gui_hooks.reviewer_did_show_question.append(reviewer_did_show_question_open_link_internally)
def reviewer_did_show_answer_open_link_internally(card):
if "iframe2internal::back" in card._note.tags:
open_link_internally(card)
gui_hooks.reviewer_did_show_answer.append(reviewer_did_show_answer_open_link_internally)
def open_link_internally(card):
global iframe2open_link_internally_oldurl
iframe2open_link_internally_oldurl=mw.reviewer.web.url(); #py, copy this url #console.log(oldurl=document.location.href); //js, copy this url
print(iframe2open_link_internally_oldurl, " → ", card._note.fields[0])
# ###SCARY SECURITY RISK STUFF### that doesn't fix signingsavvy, and only makes the chat work in eternagame
# mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
# mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
# #mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.webSecurityEnabled, False)
# mw.web.page().settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
# mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
# #mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.webSecurityEnabled, False)
# mw.reviewer.web._page.settings().setAttribute(aqt.QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# following commented lines are old broken code
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.webSecurityEnabled, False)
# aqt.QWebEngineSettings.globalSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.webSecurityEnabled, False)
# aqt.QWebEngineSettings.defaultSettings().setAttribute(aqt.QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.webSecurityEnabled, False)
# aqt.QWebEngineSettings.setAttribute(aqt.QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# ###SCARY SECURITY RISK STUFF###
mw.reviewer.web.set_open_links_externally(False) #py, allow loading the link directly without an iframe that can fail with x-frame-options example.com refused to connect
#document.location.href="https://www.checkercruncher.com/problems/1"; //js or click <a>, use actual link from {{iframe url}} field
#mw.reviewer.web.setUrl(aqt.qt.QUrl("https://www.checkercruncher.com/problems/1051")) #js or click <a>, use actual link from {{iframe url}} field
#inject javascript for sign language front side to show ASL sign video WITHOUT english word
aslhidewordscript = QWebEngineScript()
aslhidewordscript.setName("aslhideword.js");
#aslhidewordscript.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation) #does nothing
aslhidewordscript.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) #shows word then hides word
#aslhidewordscript.setInjectionPoint(QWebEngineScript.InjectionPoint.Deferred)
#mw.reviewer.web._page.runJavaScript("""
#document-start#caught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'. at observeDOM (
#document-end too late
aslhidewordscript.setSourceCode("""
// ==UserScript==
// @run-at document-start
// ==/UserScript==
function waitfordocumentElement() {
//if(typeof document.head !== "undefined" && typeof document.head.appendChild === 'function'){
try {
(function() {css=document.createElement('style');css.type='text/css';css.id='aslhideword';document.head.appendChild(css);css.innerText="#page_signs #searchField, .signing_header h2, .signing_header h3, .signing_caption #video-1-captions, .signing_details .desc {display:none;}";})();
} catch (TypeError) {
setTimeout(waitfordocumentElement,100);
}
}
waitfordocumentElement();
""")
#mw.reviewer.web._page.scripts().insert(aslhidewordscript) #anki 25.07: AttributeError: 'MainWebView' object has no attribute '_page'. Did you mean: 'page'?
#mw.reviewer.web.page.scripts().insert(aslhidewordscript) #AttributeError: 'function' object has no attribute 'scripts'
#mw.reviewer.web.scripts().insert(aslhidewordscript)
mw.reviewer.web.setUrl(QUrl(card._note.fields[0])) #js or click <a>, use actual link from {{iframe url}} field
#mw.reviewer.web.page().runJavaScript('alert("hello");')
mw.reviewer.web.page().runJavaScript('document.cookie = "_checker_cruncher_session=REDACTED";')
mw.reviewer.web.page().runJavaScript('document.cookie = "_csrf_token=REDACTED";')
mw.reviewer.web.page().runJavaScript('document.cookie = "board_drag=touch";')
mw.reviewer.web.page().runJavaScript('document.cookie = "board_selection=Vinyl";')
mw.reviewer.web.page().runJavaScript('document.cookie = "board_size=Normal";')
mw.reviewer.web.page().runJavaScript('document.cookie = "difficulty=normal";')
mw.reviewer.web.page().runJavaScript('document.cookie = "enable_quick_move=true";')#https://www.checkercruncher.com click to move piece when there is only one move
mw.reviewer.web.page().runJavaScript('document.cookie = "feedback_colors=green-red";')
mw.reviewer.web.page().runJavaScript('document.cookie = "move_delay=250";')
mw.reviewer.web.page().runJavaScript('document.cookie = "piece_selection=Original";')
mw.reviewer.web.page().runJavaScript('document.cookie = "remember_user_token=REDACTED";')
mw.reviewer.web.page().runJavaScript('document.cookie = "show_algebraic_notation=true";')
mw.reviewer.web.page().runJavaScript('document.cookie = "show_board_perspective=true";')
mw.reviewer.web.page().runJavaScript('document.cookie = "show_numbered_notation=true";')
mw.reviewer.web.page().runJavaScript('document.cookie = "signed_in=1";')
mw.reviewer.web.page().runJavaScript('document.cookie = "sound_volume=80";')
mw.reviewer.web.page().runJavaScript('document.cookie = "user_id=REDACTED";')
#do puzzle on website
def ShowAnsweragain_thread(mw):
time.sleep(5) #seconds
#mw.reviewer.web.eval('pycmd("ans");') #crashes anki the second time?? but manually clicking the button works
#needed a semicolon? works in ctrl-shift-; twice?
def webview_did_receive_js_message_restore_internal_state(handled, message, context):
global iframe2open_link_internally_oldurl
if (
mw.reviewer.card # mw?.reviewer?.card?._note?.tags
#and mw.reviewer.card._note
#and mw.reviewer.card._note.tags
and iframe2open_link_internally_oldurl.url() != "unset"
and (
(message == "ans" and "iframe2internal::front" in mw.reviewer.card._note.tags)
or (message.startswith("ease") and "iframe2internal::back" in mw.reviewer.card._note.tags)
)
):
print(mw.reviewer.card._note.fields[0], " → ", iframe2open_link_internally_oldurl)
#document.location.href="http://127.0.0.1:55431/_anki/legacyPageData?id=3054436143888" //js, paste the copied url
#mw.reviewer.web.setUrl(aqt.qt.QUrl("https://www.checkercruncher.com/problems/1051"))
#if(iframe2open_link_internally_oldurl.url() != "unset")
mw.reviewer.web.setUrl(iframe2open_link_internally_oldurl) #py, paste the copied url
mw.reviewer.web.set_open_links_externally(True) #py, optional, reset normal link click handling
iframe2open_link_internally_oldurl = QUrl("unset")
_thread.start_new_thread(ShowAnsweragain_thread, (mw,) ) #give it time to load the internal url, then automatically click Show Answer again to flip the card
#time.sleep(5) #delays first click actually changing the url back
return (True, None) #press Show Answer once to reset url, again to flip card. Press answer button once to reset url, again to answer card.
return handled
gui_hooks.webview_did_receive_js_message.append(webview_did_receive_js_message_restore_internal_state)
#mw.reviewer.nextCard()
#front iframe to front url to oldurl
#############################
#back iframe to back url to oldurl
###########failed code below here
def reviewer_will_init_answer_buttons_restore_internal_state(buttons_tuple, reviewer, card):
global iframe2open_link_internally_oldurl
#print(iframe2open_link_internally_oldurl, " ← ", card._note.fields[0])
print(card._note.fields[0], " → ", iframe2open_link_internally_oldurl)
#document.location.href="http://127.0.0.1:55431/_anki/legacyPageData?id=3054436143888" //js, paste the copied url
#mw.reviewer.web.setUrl(aqt.qt.QUrl("https://www.checkercruncher.com/problems/1051"))
mw.reviewer.web.setUrl(iframe2open_link_internally_oldurl) #py, paste the copied url
mw.reviewer.web.set_open_links_externally(True) #py, optional, reset normal link click handling
#click show answer
# iframe2open_link_internally_oldurl = QUrl("unset")
return buttons_tuple
#gui_hooks.reviewer_will_init_answer_buttons.prepend(reviewer_will_init_answer_buttons_restore_internal_state)
def reviewer_will_play_answer_sounds_restore_internal_state(card, tags):
global iframe2open_link_internally_oldurl
#print(iframe2open_link_internally_oldurl, " ← ", card._note.fields[0])
print(card._note.fields[0], " → ", iframe2open_link_internally_oldurl)
#document.location.href="http://127.0.0.1:55431/_anki/legacyPageData?id=3054436143888" //js, paste the copied url
#mw.reviewer.web.setUrl(aqt.qt.QUrl("https://www.checkercruncher.com/problems/1051"))
mw.reviewer.web.setUrl(iframe2open_link_internally_oldurl) #py, paste the copied url
mw.reviewer.web.set_open_links_externally(True) #py, optional, reset normal link click handling
#click show answer
# iframe2open_link_internally_oldurl = QUrl("unset")
#return buttons_tuple
#gui_hooks.reviewer_will_play_answer_sounds.prepend(reviewer_will_play_answer_sounds_restore_internal_state)
def card_will_show_restore_internal_state(text, card, kind):
global iframe2open_link_internally_oldurl
#print(iframe2open_link_internally_oldurl, " ← ", card._note.fields[0])
print(card._note.fields[0], " → ", iframe2open_link_internally_oldurl)
#document.location.href="http://127.0.0.1:55431/_anki/legacyPageData?id=3054436143888" //js, paste the copied url
#mw.reviewer.web.setUrl(aqt.qt.QUrl("https://www.checkercruncher.com/problems/1051"))
#if(iframe2open_link_internally_oldurl.url() != "unset")
mw.reviewer.web.setUrl(iframe2open_link_internally_oldurl) #py, paste the copied url
mw.reviewer.web.set_open_links_externally(True) #py, optional, reset normal link click handling
#click show answer
# iframe2open_link_internally_oldurl = QUrl("unset")
#return buttons_tuple
return text
#gui_hooks.card_will_show.prepend(card_will_show_restore_internal_state)
###chatgpt attempt 1 failure
def card_will_show_fix(text, card, kind):
global iframe2open_link_internally_oldurl
if iframe2open_link_internally_oldurl.url() != "unset": #if was on checkercruncher
if not ("iframe2internal::front" in card._note.tags or "iframe2internal::back" in card._note.tags): #and moving to not checkercruncher. attempt 3 SUCCESS!
mw.reviewer.web.setUrl(iframe2open_link_internally_oldurl)
mw.reviewer.web.set_open_links_externally(True)
iframe2open_link_internally_oldurl = QUrl("unset")
#mw.moveToState("deckBrowser")
mw.moveToState("review") # reloads front side checkercruncher on back side review buttons #failure 2
return text
gui_hooks.card_will_show.append(card_will_show_fix)
###chatgpt attempt 1 failure
iframe note type, card’s front-side template
<script>
// baseurl::{{Tags}} + {{baseurl tag + iframe url}}
// use iframeurl or $("iframe#my_iframe").attr("src") instead of `{{baseurl tag + iframe url}}`
baseurl="";
iframeurl=`{{baseurl tag + iframe url}}`;
baseurl=`{{Tags}}`.split(' ').filter(tag => tag.startsWith("baseurl::"))[0].slice("baseurl::".length);
//alert(baseurl)
iframeurl=baseurl+iframeurl;
//alert(iframeurl)
$("iframe#my_iframe").attr("src",iframeurl);
$("div#context a").attr("href",iframeurl);
</script>
<!--mw.reviewer.web._page.open_links_externally=False-->
{{#Context}}<div id="context" style="font-family:Babelstone Flags"><a href="{{baseurl tag + iframe url}}">{{Context}}</a> 📶{{#Do whole collection instead of single puzzle?}}<!--🧩🧩🧩Do whole collection-->{{/Do whole collection instead of single puzzle?}}<span id="elo2kyudan"></span></div>{{/Context}}
<script>
//Cookie monster https://ankiweb.net/shared/info/1501583548
document.cookie = "pieceSet=western;SameSite=None;Secure";//domain=.lishogi.org";
document.cookie = "pieceStyle=HIDETCHI;SameSite=None;Secure;domain=playshogi.com";
document.cookie = "sid=REDACTED;SameSite=None;Secure;domain=playshogi.com";
document.cookie = "pieceStyle=HIDETCHI;SameSite=None;Secure";//domain=playshogi.com";
document.cookie = "pieceStyle=HIDETCHI;SameSite=None;Secure;domain=127.0.0.1";
localStorage.setItem('xiangqi.pieceLocale', "\"en\""); document.cookie = "xiangqi.pieceLocale=\"en\";SameSite=None;Secure";//domain=play.xiangqi.com";
document.cookie="sid=;Domain=playshogi.com;Secure=false;HostOnly=true;HttpOnly=false;Path=/;SameSite=None";
document.cookie = "janggi-piece=2;Domain=pychess.com;Secure=false;HostOnly=true;HttpOnly=false;Path=/;SameSite=None";
document.cookie="ogs.preference.language=en;Domain=online-go.com";
/*
Creación:"Sun, 08 Jan 2023 01:54:43 GMT"
Expira / Tiempo máximo:"Sun, 22 Jan 2023 01:54:43 GMT"
Tamaño:39
Último acceso:"Sun, 08 Jan 2023 01:54:43 GMT"
*/
</script>
<!-- https://www.webcomponents.org/element/x-frame-bypass -- >
<script crossorigin="anonymous" src="//unpkg.com/@ungap/custom-elements"></script>
<script crossorigin="anonymous" type="module" src="https://unpkg.com/x-frame-bypass"></script>
<iframe crossorigin="anonymous" is="x-frame-bypass" src="https://www.checkercruncher.com/problems/17"></iframe>-->
<a id="nogestures" class="tappable">
<iframe id="my_iframe" is="x-frame-bypass" src="{{baseurl tag + iframe url}}" src2="_iframe.php?url={{baseurl tag + iframe url}}" style="height: 100vh; width:100%;" seamless="seamless"><a href="{{baseurl tag + iframe url}}">{{baseurl tag + iframe url}}</a></iframe>
</a>
<script>
return;
//import from https://online-go.com/puzzles by opening a collection, pasting this into the console, copying the output message into a file, then importing
var collection=$("dt:contains('Col')").next().text(); var s=""; $("#selected_puzzle option").each(function() { s+="https://online-go.com/puzzle/"+$(this).val()+" Go "+collection+"\n"; }); console.log(s);
//import multiple lishogi.org puzzles with $(".study__chapters div").each(function(e){c=document.URL.split('/');c.pop();c=c.join('/')+"/"+$(this).attr("data-id");console.log(c)})
//import from eterna using Array.from(document.querySelectorAll('a:not(.dropdown-item):not(.nav-link)[href^="/puzzles/"][created]')).map(e=>'https://eternagame.org'+e.getAttribute('href')+'/play');
</script>
<!--<script>
return;
//document.location.href="{{iframe url}}";
//alert(document.location.href);
//var iframe = $('iframe')[0];
var iframe = document.getElementById('my_iframe');
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
var scriptSource = ```
window.addEventListener('DOMContentLoaded', function() {
{{inject-script}}
});
```;
var script = iframeDocument.createElement("script");
var source = iframeDocument.createTextNode(scriptSource);
script.appendChild(source);
iframeDocument.body.appendChild(script);
</script>-->
<style>@import url("_global_front.css");</style>
<script>
//Anki-shogi-tsume-international.ahk
//Click 298, 395
//Sleep 500
//Click 725, 444
//Sleep 100
//Click 950, 698
//if (document.getElementById('context')?.innerText.match(/Shogi/)) {
// $('#context').prepend("[<s>click G16</s>] "); //set pieces to international style
//}
if (iframeurl.includes("checkercruncher.com")) {
//Refused to display 'https://www.checkercruncher.com/problems/1' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
//if($(".win")[0])
if(!$(".firefox")[0])
window.location=iframeurl; //open in external browser
//document.domain = 'checkercruncher.com';
}
if (iframeurl.includes("square-root-calculator")) {
$('#context').text("Calculate √"+parseInt(Math.random()*1000)+" by hand to three decimal places"); //sqrt(random)
//$('#context').html("Calculate <anki-mathjax>\sqrt {"+parseInt(Math.random()*1000)+"}</anki-mathjax> by hand to three decimal places"); //sqrt(random)
//MathJax.Hub.Typeset();
}
if (iframeurl.includes("eternagame.org")) {
//document.getElementById('my_iframe').src='_iframe.php?url='+iframeurl;
//document.getElementById('my_iframe').src='http://localhost:80'; //ngix
$("#context a").html("Eterna: "+$("#context a").html()+" (click here to open)");
//https://eternagame.org/puzzles/11098074/play
//document.getElementById('my_iframe').src="https://eternagame.org/eternajs/dist/prod/index.html?puzzle=11098074";
//document.getElementById('my_iframe').src="http://eternagame.org";
window.location.href=iframeurl; // automatically open in external browser
}
if (/biblegateway.com\/passage\/.*version=.*[,;].*/.test(iframeurl)) {
window.location=iframeurl.replace("&","&");
/*
// ==UserScript==
// @name biblegateway.com verse merger
// @namespace Violentmonkey Scripts
// @match https://www.biblegateway.com/passage/*&version=*;*
// @match https://www.biblegateway.com/passage/*&version=*,*
// @grant none
// @version 1.0
// @author -
// @description 3/4/2024, 09:15:15
// ==/UserScript==
(function() {
//https://www.biblegateway.com/passage/?search=G%C3%A9nesis%201&version=NVI,NIV
console.log("verse merger running");
colors=["black,grey"];//["orange,purple"]
var left=document.querySelector(".version-NVI .text-html").innerHTML.split(/(?=<sup class="versenum">)/);//split before versenum
var right=document.querySelector(".version-NIV .text-html").innerHTML.split(/(?=<sup class="versenum">)/);
right[0]=right[0].replace('<span class="chapternum">','<span style="color:'+colors[1]+'"><span class="chapternum">');//verse 1 english wants to be orange
left.unshift(...left.shift().split(/(?<=<\/h3>)/));//split after h3
right.unshift(...right.shift().split(/(?<=<\/h3>)/));
left.push(...left.pop().split(/(?=<div class="footnotes">)/));//split before footnotes
right.push(...right.pop().split(/(?=<div class="footnotes">)/));
var output="";
for(var v=0;v<left.length;v++){
output+='<span style="color:'+colors[0]+'">'+left[v]+'<span style="color:'+colors[1]+'">'+right[v];
}
//output=output.replace(/<\/h3>.*<h3>/g," ");//merge headers
document.querySelector(".version-NVI .text-html").innerHTML=output;
document.querySelector('div.passage-col-tools:nth-child(3)').remove();
console.log("verse merger ran");
})();
*/
}
</script>
<script>
if(`{{Tags}}`.includes("::go")) {
elo=`{{elo}}`.split("-")[0];
document.querySelector("#elo2kyudan").innerText=30-elo<=0?1-(30-elo)+" Dan":30-elo+" Kyu";
//00-29, 30-36 → 30kyu-1kyu, 1dan-7dan
}
</script>
<style>
html {
background-image:url("{{background-image}}");
background-size:cover;
}
</style>
<script>
//autoburyonmobile - this card may require a pc app, skip it. This allows studying a parent deck that contains both pc-only cards and android-compatible cards.
if(`{{Tags}}`.match("autoburyonmobile") && $(".mobile")[0]) {
var jsApiContract = { "version": "0.0.3", "developer": "Khonkhortisan" };
var api = new AnkiDroidJS(jsApiContract);
api.ankiBuryCard();
}
</script>
<!--
<script type="module" ssrc="https://unpkg.com/x-frame-bypass"></script>
<a href="https://www.checkercruncher.com/problems/1" target='_top'>https://www.checkercruncher.com/problems/1 target='_top'</a>
<a href="https://www.checkercruncher.com/problems/1" target='_blank'>https://www.checkercruncher.com/problems/1 target='_blank'</a>
-->
<script src="__persistentlocalstorage.js"></script>
This should be good until the next thing breaks.