split js code out into separate files, mathjax improvements

- js code that was previously bundled in .py files is now in the
web folder
- add helpers to create links to bundled files, and update
stdHtml() to accept a list of javascript files to include
instead of text
- render MathJax in card layout and preview screens - these should be
updated in the future to update the document dynamically like the
reviewer does
- start media server earlier so it can be used to serve content for
the toolbar, etc
- work around a bug in WebEngine on Windows that could cause the
media server to hang
This commit is contained in:
Damien Elmes 2017-07-28 16:19:06 +10:00
parent 5ef1692c78
commit 7ad6966943
20 changed files with 669 additions and 611 deletions

View file

@ -6,7 +6,6 @@ import time
import datetime
import json
import anki.js
from anki.utils import fmtTimeSpan, ids2str
from anki.lang import _, ngettext
@ -108,6 +107,7 @@ class CollectionStats:
self.height = 200
self.wholeCollection = False
# assumes jquery & plot are available in document
def report(self, type=0):
# 0=days, 1=weeks, 2=months
self.type = type
@ -122,8 +122,7 @@ class CollectionStats:
txt += self._section(self.easeGraph())
txt += self._section(self.cardGraph())
txt += self._section(self.footer())
return "<script>%s\n</script><center>%s</center>" % (
anki.js.jquery+anki.js.plot, txt)
return "<center>%s</center>" % txt
def _section(self, txt):
return "<div class=section>%s</div>" % txt

View file

@ -1120,10 +1120,13 @@ where id in %s""" % ids2str(sf))
txt = re.sub("\[\[type:[^]]+\]\]", "", txt)
ti = lambda x: x
base = self.mw.baseHTML()
jsinc = ["jquery.js","browsersel.js",
"mathjax/conf.js", "mathjax/MathJax.js",
"mathjax/queue-typeset.js"]
self._previewWeb.stdHtml(
ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base,
js=anki.js.browserSel)
js=jsinc)
clearAudioQueue()
if self.mw.reviewer.autoplay(c):
playFromText(txt)

View file

@ -13,8 +13,6 @@ from aqt.utils import saveGeom, restoreGeom, mungeQA,\
showWarning, openHelp, downArrow
from anki.utils import isMac, isWin, joinFields
from aqt.webview import AnkiWebView
import anki.js
class CardLayout(QDialog):
@ -226,14 +224,17 @@ Please create a new card type first."""))
c = self.card
ti = self.maybeTextInput
base = self.mw.baseHTML()
jsinc = ["jquery.js","browsersel.js",
"mathjax/conf.js", "mathjax/MathJax.js",
"mathjax/queue-typeset.js"]
self.tab['pform'].frontWeb.setEnabled(False)
self.tab['pform'].backWeb.setEnabled(False)
self.tab['pform'].frontWeb.stdHtml(
ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base),
bodyClass="card card%d" % (c.ord+1), head=base, js=jsinc),
self.tab['pform'].backWeb.stdHtml(
ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base),
bodyClass="card card%d" % (c.ord+1), head=base, js=jsinc),
self.tab['pform'].frontWeb.setEnabled(True)
self.tab['pform'].backWeb.setEnabled(True)
clearAudioQueue()

View file

@ -6,7 +6,6 @@ from aqt.qt import *
from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \
openHelp, downArrow
from anki.utils import isMac, ids2str, fmtTimeSpan
import anki.js
from anki.errors import DeckRenameError
import aqt
from anki.sound import clearAudioQueue
@ -141,7 +140,7 @@ body { margin: 1em; -webkit-user-select: none; }
stats = self._renderStats()
self.web.stdHtml(self._body%dict(
tree=tree, stats=stats, countwarn=self._countWarn()), css=css,
js=anki.js.jquery+anki.js.ui)
js=["jquery.js", "jquery-ui.js"])
self.web.key = "deckBrowser"
self._drawButtons()

View file

@ -23,470 +23,13 @@ from aqt.webview import AnkiWebView
from aqt.utils import shortcut, showInfo, showWarning, getFile, \
openHelp, tooltip, downArrow
import aqt
import anki.js
from bs4 import BeautifulSoup
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
_html = """
<style>
.field {
border: 1px solid #aaa; background:#fff; color:#000; padding: 5px;
}
/* prevent floated images from being displayed outside field */
.field:after {
content: "";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.fname { vertical-align: middle; padding: 0; }
img { max-width: 90%%; }
html { background: %s; }
body { margin: 5px; }
#topbuts { position: fixed; height: 24px; top: 0; padding: 2px; left:0;right:0}
.topbut { width: 16px; height: 16px; }
.rainbow {
background-image: -webkit-gradient(linear, left top, left bottom,
color-stop(0.00, #f77),
color-stop(50%%, #7f7),
color-stop(100%%, #77f));
}
.linkb { -webkit-appearance: none; border: 0; padding: 0px 2px; background: transparent; }
.linkb:disabled { opacity: 0.3; cursor: not-allowed; }
.highlighted {
border-bottom: 3px solid #000;
}
.prewrap { white-space: pre-wrap; }
#fields { margin-top: 35px; }
</style><script>
var currentField = null;
var changeTimer = null;
var dropTarget = null;
var prewrapMode = false;
String.prototype.format = function() {
var args = arguments;
return this.replace(/\{\d+\}/g, function(m){
return args[m.match(/\d+/)]; });
};
function setFGButton(col) {
$("#forecolor")[0].style.backgroundColor = col;
};
function saveNow() {
clearChangeTimer();
if (currentField) {
currentField.blur();
}
};
function onKey() {
// esc clears focus, allowing dialog to close
if (window.event.which == 27) {
currentField.blur();
return;
}
// catch enter key in prewrap mode
if (window.event.which == 13 && prewrapMode) {
window.event.preventDefault();
insertNewline();
return;
}
clearChangeTimer();
changeTimer = setTimeout(function () {
updateButtonState();
saveField("key");
}, 600);
};
function insertNewline() {
if (!inPreEnvironment()) {
setFormat("insertText", "\\n");
return;
}
// in some cases inserting a newline will not show any changes,
// as a trailing newline at the end of a block does not render
// differently. so in such cases we note the height has not
// changed and insert an extra newline.
var r = window.getSelection().getRangeAt(0);
if (!r.collapsed) {
// delete any currently selected text first, making
// sure the delete is undoable
setFormat("delete");
}
var oldHeight = currentField.clientHeight;
setFormat("inserthtml", "\\n");
if (currentField.clientHeight == oldHeight) {
setFormat("inserthtml", "\\n");
}
}
// is the cursor in an environment that respects whitespace?
function inPreEnvironment() {
var n = window.getSelection().anchorNode;
if (n.nodeType == 3) {
n = n.parentNode;
}
return window.getComputedStyle(n).whiteSpace.startsWith("pre");
}
function checkForEmptyField() {
if (currentField.innerHTML == "") {
currentField.innerHTML = "<br>";
}
};
function updateButtonState() {
var buts = ["bold", "italic", "underline", "superscript", "subscript"];
for (var i=0; i<buts.length; i++) {
var name = buts[i];
if (document.queryCommandState(name)) {
$("#"+name).addClass("highlighted");
} else {
$("#"+name).removeClass("highlighted");
}
}
// fixme: forecolor
// 'col': document.queryCommandValue("forecolor")
};
function toggleEditorButton(buttonid) {
if ($(buttonid).hasClass("highlighted")) {
$(buttonid).removeClass("highlighted");
} else {
$(buttonid).addClass("highlighted");
}
};
function setFormat(cmd, arg, nosave) {
document.execCommand(cmd, false, arg);
if (!nosave) {
saveField('key');
updateButtonState();
}
};
function clearChangeTimer() {
if (changeTimer) {
clearTimeout(changeTimer);
changeTimer = null;
}
};
function onFocus(elem) {
currentField = elem;
pycmd("focus:" + currentField.id.substring(1));
enableButtons();
// don't adjust cursor on mouse clicks
if (mouseDown) { return; }
// do this twice so that there's no flicker on newer versions
caretToEnd();
// scroll if bottom of element off the screen
function pos(obj) {
var cur = 0;
do {
cur += obj.offsetTop;
} while (obj = obj.offsetParent);
return cur;
}
var y = pos(elem);
if ((window.pageYOffset+window.innerHeight) < (y+elem.offsetHeight) ||
window.pageYOffset > y) {
window.scroll(0,y+elem.offsetHeight-window.innerHeight);
}
}
function focusField(n) {
$("#f"+n).focus();
}
function onDragOver(elem) {
// if we focus the target element immediately, the drag&drop turns into a
// copy, so note it down for later instead
dropTarget = elem;
}
function onPaste(elem) {
pycmd("paste");
window.event.preventDefault();
}
function caretToEnd() {
var r = document.createRange()
r.selectNodeContents(currentField);
r.collapse(false);
var s = document.getSelection();
s.removeAllRanges();
s.addRange(r);
};
function onBlur() {
if (currentField) {
saveField("blur");
}
clearChangeTimer();
disableButtons();
};
function saveField(type) {
if (!currentField) {
// no field has been focused yet
return;
}
// type is either 'blur' or 'key'
pycmd(type + ":" + currentField.innerHTML);
clearChangeTimer();
};
function wrappedExceptForWhitespace(text, front, back) {
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
return match[1] + front + match[2] + back + match[3];
};
function disableButtons() {
$("button.linkb").prop("disabled", true);
};
function enableButtons() {
$("button.linkb").prop("disabled", false);
};
// disable the buttons if a field is not currently focused
function maybeDisableButtons() {
if (!document.activeElement || document.activeElement.className != "field") {
disableButtons();
} else {
enableButtons();
}
};
function wrap(front, back) {
var s = window.getSelection();
var r = s.getRangeAt(0);
var content = r.cloneContents();
var span = document.createElement("span")
span.appendChild(content);
var new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
setFormat("inserthtml", new_);
if (!span.innerHTML) {
// run with an empty selection; move cursor back past postfix
r = s.getRangeAt(0);
r.setStart(r.startContainer, r.startOffset - back.length);
r.collapse(true);
s.removeAllRanges();
s.addRange(r);
}
};
function setFields(fields, focusTo, prewrap) {
var txt = "";
for (var i=0; i<fields.length; i++) {
var n = fields[i][0];
var f = fields[i][1];
if (!f) {
f = "<br>";
}
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%%>".format(n);
txt += "<div id=f{0} onkeydown='onKey();' oninput='checkForEmptyField()' onmouseup='onKey();'".format(i);
txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field ";
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' ";
txt += "contentEditable=true class=field>{0}</div>".format(f);
txt += "</td></tr>";
}
$("#fields").html("<table cellpadding=0 width=100%%>"+txt+"</table>");
if (!focusTo) {
focusTo = 0;
}
if (focusTo >= 0) {
$("#f"+focusTo).focus();
}
maybeDisableButtons();
prewrapMode = prewrap;
if (prewrap) {
$(".field").addClass("prewrap");
}
};
function setBackgrounds(cols) {
for (var i=0; i<cols.length; i++) {
$("#f"+i).css("background", cols[i]);
}
}
function setFonts(fonts) {
for (var i=0; i<fonts.length; i++) {
$("#f"+i).css("font-family", fonts[i][0]);
$("#f"+i).css("font-size", fonts[i][1]);
$("#f"+i)[0].dir = fonts[i][2] ? "rtl" : "ltr";
}
}
function showDupes() {
$("#dupes").show();
}
function hideDupes() {
$("#dupes").hide();
}
var pasteHTML = function(html, internal) {
if (!internal) {
html = filterHTML(html);
}
setFormat("inserthtml", html);
};
var filterHTML = function(html) {
// wrap it in <top> as we aren't allowed to change top level elements
var top = $.parseHTML("<ankitop>" + html + "</ankitop>")[0];
filterNode(top);
var outHtml = top.innerHTML;
//console.log(`input html: ${html}`);
//console.log(`outpt html: ${outHtml}`);
return outHtml;
};
var allowedTags = {};
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
"STRONG", "PRE", "SUB", "SUP", "TABLE", "DD", "DT", "DL"];
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
}
allowedTags["A"] = {"attrs": ["HREF"]};
allowedTags["TR"] = {"attrs": ["ROWSPAN"]};
allowedTags["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
allowedTags["IMG"] = {"attrs": ["SRC"]};
var blockRegex = /^(address|blockquote|br|center|div|dl|h[1-6]|hr|ol|p|pre|table|ul|dd|dt|li|tbody|td|tfoot|th|thead|tr)$/i;
function isBlockLevel(n) {
return blockRegex.test(n.nodeName);
};
function isInlineElement(n) {
return n && !isBlockLevel(n);
}
function convertDivToNewline(node, isParagraph) {
var html = node.innerHTML;
if (isInlineElement(node.previousSibling) && html) {
html = "\\n" + html;
}
if (isInlineElement(node.nextSibling)) {
html += "\\n";
}
if (isParagraph) {
html += "\\n";
}
node.outerHTML = html;
};
var filterNode = function(node) {
// text node?
if (node.nodeType == 3) {
if (prewrapMode) {
// collapse standard whitespace
var val = node.nodeValue.replace(/^[ \\r\\n\\t]+$/g, " ");
// non-breaking spaces can be represented as normal spaces
val = val.replace(/&nbsp;|\u00a0/g, " ");
node.nodeValue = val;
}
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
var nodes = [];
for (var i = 0; i < node.childNodes.length; i++) {
nodes.push(node.childNodes[i]);
}
for (var i = 0; i < nodes.length; i++) {
filterNode(nodes[i]);
}
if (node.tagName == "ANKITOP") {
return;
}
var tag = allowedTags[node.tagName];
if (!tag) {
if (!node.innerHTML) {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else if (prewrapMode && node.tagName == "BR") {
node.outerHTML = "\\n";
} else if (prewrapMode && node.tagName == "DIV") {
convertBlockToNewline(node, false);
} else if (prewrapMode && node.tagName == "P") {
convertBlockToNewline(node, true);
} else {
// allowed, filter out attributes
var toRemove = [];
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
var attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) == -1) {
toRemove.push(attr);
}
}
for (var i = 0; i < toRemove.length; i++) {
node.removeAttributeNode(toRemove[i]);
}
}
};
var mouseDown = 0;
$(function () {
document.body.onmousedown = function () {
mouseDown++;
}
document.body.onmouseup = function () {
mouseDown--;
}
document.onclick = function (evt) {
var src = window.event.srcElement;
if (src.tagName == "IMG") {
// image clicked; find contenteditable parent
var p = src;
while (p = p.parentNode) {
if (p.className == "field") {
$("#"+p.id).focus();
break;
}
}
}
}
// prevent editor buttons from taking focus
$("button.linkb").on("mousedown", function(e) { e.preventDefault(); });
});
</script>
<style>html { background: %s; }</style>
<div id="topbuts">%s</div>
<div id="fields"></div>
<div id="dupes" style="display:none;"><a href="#" onclick="pycmd('dupes');return false;">%s</a></div>
@ -560,10 +103,12 @@ class Editor:
""" % dict(flds=_("Fields"), cards=_("Cards"), rightbts="".join(righttopbtns))
bgcol = self.mw.app.palette().window().color().name()
# then load page
self.web.stdHtml(_html % (
html = self.web.bundledCSS("editor.css") + _html
self.web.stdHtml(html % (
bgcol,
topbuts,
_("Show Duplicates")), js=anki.js.jquery, head=self.mw.baseHTML())
_("Show Duplicates")), head=self.mw.baseHTML(),
js=["jquery.js", "editor.js"])
# Top buttons
######################################################################

View file

@ -66,6 +66,7 @@ class AnkiQt(QMainWindow):
self.setupAppMsg()
self.setupKeys()
self.setupThreads()
self.setupMediaServer()
self.setupMainWindow()
self.setupSystemSpecific()
self.setupStyle()
@ -77,7 +78,6 @@ class AnkiQt(QMainWindow):
self.setupHooks()
self.setupRefreshTimer()
self.updateTitleBar()
self.setupMediaServer()
# screens
self.setupDeckBrowser()
self.setupOverview()

View file

@ -5,6 +5,7 @@
from aqt.qt import *
from http import HTTPStatus
import http.server
import socketserver
import errno
# locate web folder in source/binary distribution
@ -22,6 +23,11 @@ def _getExportFolder():
_exportFolder = _getExportFolder()
# webengine on windows sometimes opens a connection and fails to send a request,
# which will hang the server if unthreaded
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
pass
class MediaServer(QThread):
def run(self):
@ -29,7 +35,7 @@ class MediaServer(QThread):
self.server = None
while not self.server:
try:
self.server = http.server.HTTPServer(
self.server = ThreadedHTTPServer(
("localhost", self.port), RequestHandler)
except OSError as e:
if e.errno == errno.EADDRINUSE:

View file

@ -121,56 +121,6 @@ class Reviewer:
_revHtml = """
<img src="qrc:/icons/rating.png" id=star class=marked>
<div id=qa></div>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
jax: ["input/TeX","output/CommonHTML"],
extensions: ["tex2jax.js"],
TeX: {
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
},
messageStyle: "none",
skipStartupTypeset: true,
showMathMenu: false
});
</script>
<script type="text/javascript" src="_anki/mathjax/MathJax.js"></script>
<script>
var ankiPlatform = "desktop";
var typeans;
function _updateQA (q, answerMode, klass) {
$("#qa").html(q);
typeans = document.getElementById("typeans");
if (typeans) {
typeans.focus();
}
if (answerMode) {
var e = $("#answer");
if (e[0]) { e[0].scrollIntoView(); }
} else {
window.scrollTo(0, 0);
}
if (klass) {
document.body.className = klass;
}
// don't allow drags of images, which cause them to be deleted
$("img").attr("draggable", false);
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
};
function _toggleStar (show) {
if (show) {
$(".marked").show();
} else {
$(".marked").hide();
}
}
function _typeAnsPress() {
if (window.event.keyCode === 13) {
pycmd("ans");
}
}
</script>
"""
def _initWeb(self):
@ -179,13 +129,20 @@ function _typeAnsPress() {
base = self.mw.baseHTML()
# main window
self.web.onLoadFinished = self._showQuestion
self.web.stdHtml(self._revHtml, self._styles(), head=base)
self.web.stdHtml(self._revHtml, self._styles(), head=base,
js=["jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js"])
# show answer / ease buttons
self.bottom.web.show()
self.bottom.web.onLoadFinished = self._onBottomLoadFinished
self.bottom.web.stdHtml(
self._bottomHTML(),
self.bottom._css + self._bottomCSS)
self.bottom._css + self._bottomCSS,
js=["jquery.js", "reviewer-bottom.js"]
)
# Showing the question
##########################################################################
@ -573,46 +530,7 @@ padding: 3px;
</table>
</center>
<script>
var time = %(time)d;
var maxTime = 0;
$(function () {
$("#ansbut").focus();
updateTime();
setInterval(function () { time += 1; updateTime() }, 1000);
});
var updateTime = function () {
if (!maxTime) {
$("#time").text("");
return;
}
time = Math.min(maxTime, time);
var m = Math.floor(time / 60);
var s = time %% 60;
if (s < 10) {
s = "0" + s;
}
var e = $("#time");
if (maxTime == time) {
e.html("<font color=red>" + m + ":" + s + "</font>");
} else {
e.text(m + ":" + s);
}
}
function showQuestion(txt, maxTime_) {
// much faster than jquery's .html()
$("#middle")[0].innerHTML = txt;
$("#ansbut").focus();
time = 0;
maxTime = maxTime_;
}
function showAnswer(txt) {
$("#middle")[0].innerHTML = txt;
$("#defease").focus();
}
time = %(time)d;
</script>
""" % dict(rem=self._remaining(), edit=_("Edit"),
editkey=_("Shortcut key: %s") % "E",

View file

@ -79,5 +79,6 @@ class DeckStats(QDialog):
stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection
self.report = stats.report(type=self.period)
self.form.web.stdHtml("<html><body>"+self.report+"</body></html>")
self.form.web.stdHtml("<html><body>"+self.report+"</body></html>",
js=["jquery.js", "plot.js"])
self.mw.progress.finish()

View file

@ -7,7 +7,6 @@ from anki.hooks import runHook
from aqt.qt import *
from aqt.utils import openLink
from anki.utils import isMac, isWin
import anki.js
# Page for debug messages
##########################################################################
@ -151,7 +150,7 @@ class AnkiWebView(QWebEngineView):
dpi = screen.logicalDpiX()
return max(1, dpi / 96.0)
def stdHtml(self, body, css="", bodyClass="", js=None, head=""):
def stdHtml(self, body, css="", bodyClass="", js=["jquery.js"], head=""):
if isWin:
buttonspec = "button { font-size: 12px; font-family:'Segoe UI'; }"
fontspec = 'font-size:12px;font-family:"Segoe UI";'
@ -167,6 +166,7 @@ border-radius:5px; font-family: Helvetica }"""
family = self.font().family()
fontspec = 'font-size:14px;font-family:%s;'%\
family
jstxt = "\n".join(self.bundledScript(fname) for fname in js)
html="""
<!doctype html>
@ -174,9 +174,8 @@ border-radius:5px; font-family: Helvetica }"""
body { zoom: %f; %s }
%s
%s</style>
<script>
%s
<script>
// prevent backspace key from going back a page
document.addEventListener("keydown", function(evt) {
if (evt.keyCode != 8) {
@ -202,11 +201,21 @@ document.addEventListener("keydown", function(evt) {
self.zoomFactor(),
fontspec,
buttonspec,
css, js or anki.js.jquery+anki.js.browserSel,
css, jstxt,
head, bodyClass, body)
#print(html)
self.setHtml(html)
def webBundlePath(self, path):
from aqt import mw
return "http://localhost:%d/_anki/%s" % (mw.mediaServer.port, path)
def bundledScript(self, fname):
return '<script src="%s"></script>' % self.webBundlePath(fname)
def bundledCSS(self, fname):
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(fname)
def eval(self, js):
self.page().runJavaScript(js)

1
web/browsersel.js Normal file
View file

@ -0,0 +1 @@
/* CSS Browser Selector v0.4.0 (Nov 02, 2010) Rafael Lima (http://rafael.adm.br) */function css_browser_selector(u){var ua=u.toLowerCase(),is=function(t){return ua.indexOf(t)>-1},g='gecko',w='webkit',s='safari',o='opera',m='mobile',h=document.documentElement,b=[(!(/opera|webtv/i.test(ua))&&/msie\s(\d)/.test(ua))?('ie ie'+RegExp.$1):is('firefox/2')?g+' ff2':is('firefox/3.5')?g+' ff3 ff3_5':is('firefox/3.6')?g+' ff3 ff3_6':is('firefox/3')?g+' ff3':is('gecko/')?g:is('opera')?o+(/version\/(\d+)/.test(ua)?' '+o+RegExp.$1:(/opera(\s|\/)(\d+)/.test(ua)?' '+o+RegExp.$2:'')):is('konqueror')?'konqueror':is('blackberry')?m+' blackberry':is('android')?m+' android':is('chrome')?w+' chrome':is('iron')?w+' iron':is('applewebkit/')?w+' '+s+(/version\/(\d+)/.test(ua)?' '+s+RegExp.$1:''):is('mozilla/')?g:'',is('j2me')?m+' j2me':is('iphone')?m+' iphone':is('ipod')?m+' ipod':is('ipad')?m+' ipad':is('mac')?'mac':is('darwin')?'mac':is('webtv')?'webtv':is('win')?'win'+(is('windows nt 6.0')?' vista':''):is('freebsd')?'freebsd':(is('x11')||is('linux'))?'linux':'','js']; c = b.join(' '); h.className += ' '+c; return c;}; css_browser_selector(navigator.userAgent);

73
web/editor.css Normal file
View file

@ -0,0 +1,73 @@
.field {
border: 1px solid #aaa;
background: #fff;
color: #000;
padding: 5px;
}
/* prevent floated images from being displayed outside field */
.field:after {
content: "";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.fname {
vertical-align: middle;
padding: 0;
}
img {
max-width: 90%;
}
body {
margin: 5px;
}
#topbuts {
position: fixed;
height: 24px;
top: 0;
padding: 2px;
left: 0;
right: 0
}
.topbut {
width: 16px;
height: 16px;
}
.rainbow {
background-image: -webkit-gradient(linear, left top, left bottom,
color-stop(0.00, #f77),
color-stop(50%, #7f7),
color-stop(100%, #77f));
}
.linkb {
-webkit-appearance: none;
border: 0;
padding: 0px 2px;
background: transparent;
}
.linkb:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.highlighted {
border-bottom: 3px solid #000;
}
.prewrap {
white-space: pre-wrap;
}
#fields {
margin-top: 35px;
}

423
web/editor.js Normal file
View file

@ -0,0 +1,423 @@
var currentField = null;
var changeTimer = null;
var dropTarget = null;
var prewrapMode = false;
String.prototype.format = function () {
var args = arguments;
return this.replace(/\{\d+\}/g, function (m) {
return args[m.match(/\d+/)];
});
};
function setFGButton(col) {
$("#forecolor")[0].style.backgroundColor = col;
};
function saveNow() {
clearChangeTimer();
if (currentField) {
currentField.blur();
}
};
function onKey() {
// esc clears focus, allowing dialog to close
if (window.event.which == 27) {
currentField.blur();
return;
}
// catch enter key in prewrap mode
if (window.event.which == 13 && prewrapMode) {
window.event.preventDefault();
insertNewline();
return;
}
clearChangeTimer();
changeTimer = setTimeout(function () {
updateButtonState();
saveField("key");
}, 600);
};
function insertNewline() {
if (!inPreEnvironment()) {
setFormat("insertText", "\\n");
return;
}
// in some cases inserting a newline will not show any changes,
// as a trailing newline at the end of a block does not render
// differently. so in such cases we note the height has not
// changed and insert an extra newline.
var r = window.getSelection().getRangeAt(0);
if (!r.collapsed) {
// delete any currently selected text first, making
// sure the delete is undoable
setFormat("delete");
}
var oldHeight = currentField.clientHeight;
setFormat("inserthtml", "\\n");
if (currentField.clientHeight == oldHeight) {
setFormat("inserthtml", "\\n");
}
}
// is the cursor in an environment that respects whitespace?
function inPreEnvironment() {
var n = window.getSelection().anchorNode;
if (n.nodeType == 3) {
n = n.parentNode;
}
return window.getComputedStyle(n).whiteSpace.startsWith("pre");
}
function checkForEmptyField() {
if (currentField.innerHTML == "") {
currentField.innerHTML = "<br>";
}
};
function updateButtonState() {
var buts = ["bold", "italic", "underline", "superscript", "subscript"];
for (var i = 0; i < buts.length; i++) {
var name = buts[i];
if (document.queryCommandState(name)) {
$("#" + name).addClass("highlighted");
} else {
$("#" + name).removeClass("highlighted");
}
}
// fixme: forecolor
// 'col': document.queryCommandValue("forecolor")
};
function toggleEditorButton(buttonid) {
if ($(buttonid).hasClass("highlighted")) {
$(buttonid).removeClass("highlighted");
} else {
$(buttonid).addClass("highlighted");
}
};
function setFormat(cmd, arg, nosave) {
document.execCommand(cmd, false, arg);
if (!nosave) {
saveField('key');
updateButtonState();
}
};
function clearChangeTimer() {
if (changeTimer) {
clearTimeout(changeTimer);
changeTimer = null;
}
};
function onFocus(elem) {
currentField = elem;
pycmd("focus:" + currentField.id.substring(1));
enableButtons();
// don't adjust cursor on mouse clicks
if (mouseDown) {
return;
}
// do this twice so that there's no flicker on newer versions
caretToEnd();
// scroll if bottom of element off the screen
function pos(obj) {
var cur = 0;
do {
cur += obj.offsetTop;
} while (obj = obj.offsetParent);
return cur;
}
var y = pos(elem);
if ((window.pageYOffset + window.innerHeight) < (y + elem.offsetHeight) ||
window.pageYOffset > y) {
window.scroll(0, y + elem.offsetHeight - window.innerHeight);
}
}
function focusField(n) {
$("#f" + n).focus();
}
function onDragOver(elem) {
// if we focus the target element immediately, the drag&drop turns into a
// copy, so note it down for later instead
dropTarget = elem;
}
function onPaste(elem) {
pycmd("paste");
window.event.preventDefault();
}
function caretToEnd() {
var r = document.createRange()
r.selectNodeContents(currentField);
r.collapse(false);
var s = document.getSelection();
s.removeAllRanges();
s.addRange(r);
};
function onBlur() {
if (currentField) {
saveField("blur");
}
clearChangeTimer();
disableButtons();
};
function saveField(type) {
if (!currentField) {
// no field has been focused yet
return;
}
// type is either 'blur' or 'key'
pycmd(type + ":" + currentField.innerHTML);
clearChangeTimer();
};
function wrappedExceptForWhitespace(text, front, back) {
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
return match[1] + front + match[2] + back + match[3];
};
function disableButtons() {
$("button.linkb").prop("disabled", true);
};
function enableButtons() {
$("button.linkb").prop("disabled", false);
};
// disable the buttons if a field is not currently focused
function maybeDisableButtons() {
if (!document.activeElement || document.activeElement.className != "field") {
disableButtons();
} else {
enableButtons();
}
};
function wrap(front, back) {
var s = window.getSelection();
var r = s.getRangeAt(0);
var content = r.cloneContents();
var span = document.createElement("span")
span.appendChild(content);
var new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
setFormat("inserthtml", new_);
if (!span.innerHTML) {
// run with an empty selection; move cursor back past postfix
r = s.getRangeAt(0);
r.setStart(r.startContainer, r.startOffset - back.length);
r.collapse(true);
s.removeAllRanges();
s.addRange(r);
}
};
function setFields(fields, focusTo, prewrap) {
var txt = "";
for (var i = 0; i < fields.length; i++) {
var n = fields[i][0];
var f = fields[i][1];
if (!f) {
f = "<br>";
}
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%>".format(n);
txt += "<div id=f{0} onkeydown='onKey();' oninput='checkForEmptyField()' onmouseup='onKey();'".format(i);
txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field ";
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' ";
txt += "contentEditable=true class=field>{0}</div>".format(f);
txt += "</td></tr>";
}
$("#fields").html("<table cellpadding=0 width=100%>" + txt + "</table>");
if (!focusTo) {
focusTo = 0;
}
if (focusTo >= 0) {
$("#f" + focusTo).focus();
}
maybeDisableButtons();
prewrapMode = prewrap;
if (prewrap) {
$(".field").addClass("prewrap");
}
};
function setBackgrounds(cols) {
for (var i = 0; i < cols.length; i++) {
$("#f" + i).css("background", cols[i]);
}
}
function setFonts(fonts) {
for (var i = 0; i < fonts.length; i++) {
$("#f" + i).css("font-family", fonts[i][0]);
$("#f" + i).css("font-size", fonts[i][1]);
$("#f" + i)[0].dir = fonts[i][2] ? "rtl" : "ltr";
}
}
function showDupes() {
$("#dupes").show();
}
function hideDupes() {
$("#dupes").hide();
}
var pasteHTML = function (html, internal) {
if (!internal) {
html = filterHTML(html);
}
setFormat("inserthtml", html);
};
var filterHTML = function (html) {
// wrap it in <top> as we aren't allowed to change top level elements
var top = $.parseHTML("<ankitop>" + html + "</ankitop>")[0];
filterNode(top);
var outHtml = top.innerHTML;
//console.log(`input html: ${html}`);
//console.log(`outpt html: ${outHtml}`);
return outHtml;
};
var allowedTags = {};
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
"STRONG", "PRE", "SUB", "SUP", "TABLE", "DD", "DT", "DL"];
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
}
allowedTags["A"] = {"attrs": ["HREF"]};
allowedTags["TR"] = {"attrs": ["ROWSPAN"]};
allowedTags["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
allowedTags["IMG"] = {"attrs": ["SRC"]};
var blockRegex = /^(address|blockquote|br|center|div|dl|h[1-6]|hr|ol|p|pre|table|ul|dd|dt|li|tbody|td|tfoot|th|thead|tr)$/i;
function isBlockLevel(n) {
return blockRegex.test(n.nodeName);
};
function isInlineElement(n) {
return n && !isBlockLevel(n);
}
function convertDivToNewline(node, isParagraph) {
var html = node.innerHTML;
if (isInlineElement(node.previousSibling) && html) {
html = "\\n" + html;
}
if (isInlineElement(node.nextSibling)) {
html += "\\n";
}
if (isParagraph) {
html += "\\n";
}
node.outerHTML = html;
};
var filterNode = function (node) {
// text node?
if (node.nodeType == 3) {
if (prewrapMode) {
// collapse standard whitespace
var val = node.nodeValue.replace(/^[ \\r\\n\\t]+$/g, " ");
// non-breaking spaces can be represented as normal spaces
val = val.replace(/&nbsp;|\u00a0/g, " ");
node.nodeValue = val;
}
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
var nodes = [];
for (var i = 0; i < node.childNodes.length; i++) {
nodes.push(node.childNodes[i]);
}
for (var i = 0; i < nodes.length; i++) {
filterNode(nodes[i]);
}
if (node.tagName == "ANKITOP") {
return;
}
var tag = allowedTags[node.tagName];
if (!tag) {
if (!node.innerHTML) {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else if (prewrapMode && node.tagName == "BR") {
node.outerHTML = "\\n";
} else if (prewrapMode && node.tagName == "DIV") {
convertBlockToNewline(node, false);
} else if (prewrapMode && node.tagName == "P") {
convertBlockToNewline(node, true);
} else {
// allowed, filter out attributes
var toRemove = [];
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
var attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) == -1) {
toRemove.push(attr);
}
}
for (var i = 0; i < toRemove.length; i++) {
node.removeAttributeNode(toRemove[i]);
}
}
};
var mouseDown = 0;
$(function () {
document.body.onmousedown = function () {
mouseDown++;
}
document.body.onmouseup = function () {
mouseDown--;
}
document.onclick = function (evt) {
var src = window.event.srcElement;
if (src.tagName == "IMG") {
// image clicked; find contenteditable parent
var p = src;
while (p = p.parentNode) {
if (p.className == "field") {
$("#" + p.id).focus();
break;
}
}
}
}
// prevent editor buttons from taking focus
$("button.linkb").on("mousedown", function (e) {
e.preventDefault();
});
});

File diff suppressed because one or more lines are too long

5
web/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

10
web/mathjax/conf.js Normal file
View file

@ -0,0 +1,10 @@
window.MathJax = {
jax: ["input/TeX","output/CommonHTML"],
extensions: ["tex2jax.js"],
TeX: {
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
},
messageStyle: "none",
skipStartupTypeset: true,
showMathMenu: false
};

View file

@ -0,0 +1,2 @@
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);

24
web/plot.js Normal file

File diff suppressed because one or more lines are too long

43
web/reviewer-bottom.js Normal file
View file

@ -0,0 +1,43 @@
var time; // set in python code
var maxTime = 0;
$(function () {
$("#ansbut").focus();
updateTime();
setInterval(function () {
time += 1;
updateTime()
}, 1000);
});
var updateTime = function () {
if (!maxTime) {
$("#time").text("");
return;
}
time = Math.min(maxTime, time);
var m = Math.floor(time / 60);
var s = time % 60;
if (s < 10) {
s = "0" + s;
}
var e = $("#time");
if (maxTime == time) {
e.html("<font color=red>" + m + ":" + s + "</font>");
} else {
e.text(m + ":" + s);
}
}
function showQuestion(txt, maxTime_) {
// much faster than jquery's .html()
$("#middle")[0].innerHTML = txt;
$("#ansbut").focus();
time = 0;
maxTime = maxTime_;
}
function showAnswer(txt) {
$("#middle")[0].innerHTML = txt;
$("#defease").focus();
}

37
web/reviewer.js Normal file
View file

@ -0,0 +1,37 @@
var ankiPlatform = "desktop";
var typeans;
function _updateQA(q, answerMode, klass) {
$("#qa").html(q);
typeans = document.getElementById("typeans");
if (typeans) {
typeans.focus();
}
if (answerMode) {
var e = $("#answer");
if (e[0]) {
e[0].scrollIntoView();
}
} else {
window.scrollTo(0, 0);
}
if (klass) {
document.body.className = klass;
}
// don't allow drags of images, which cause them to be deleted
$("img").attr("draggable", false);
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
};
function _toggleStar(show) {
if (show) {
$(".marked").show();
} else {
$(".marked").hide();
}
}
function _typeAnsPress() {
if (window.event.keyCode === 13) {
pycmd("ans");
}
}