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 datetime
import json import json
import anki.js
from anki.utils import fmtTimeSpan, ids2str from anki.utils import fmtTimeSpan, ids2str
from anki.lang import _, ngettext from anki.lang import _, ngettext
@ -108,6 +107,7 @@ class CollectionStats:
self.height = 200 self.height = 200
self.wholeCollection = False self.wholeCollection = False
# assumes jquery & plot are available in document
def report(self, type=0): def report(self, type=0):
# 0=days, 1=weeks, 2=months # 0=days, 1=weeks, 2=months
self.type = type self.type = type
@ -122,8 +122,7 @@ class CollectionStats:
txt += self._section(self.easeGraph()) txt += self._section(self.easeGraph())
txt += self._section(self.cardGraph()) txt += self._section(self.cardGraph())
txt += self._section(self.footer()) txt += self._section(self.footer())
return "<script>%s\n</script><center>%s</center>" % ( return "<center>%s</center>" % txt
anki.js.jquery+anki.js.plot, txt)
def _section(self, txt): def _section(self, txt):
return "<div class=section>%s</div>" % 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) txt = re.sub("\[\[type:[^]]+\]\]", "", txt)
ti = lambda x: x ti = lambda x: x
base = self.mw.baseHTML() base = self.mw.baseHTML()
jsinc = ["jquery.js","browsersel.js",
"mathjax/conf.js", "mathjax/MathJax.js",
"mathjax/queue-typeset.js"]
self._previewWeb.stdHtml( self._previewWeb.stdHtml(
ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(), ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base, bodyClass="card card%d" % (c.ord+1), head=base,
js=anki.js.browserSel) js=jsinc)
clearAudioQueue() clearAudioQueue()
if self.mw.reviewer.autoplay(c): if self.mw.reviewer.autoplay(c):
playFromText(txt) playFromText(txt)

View file

@ -13,8 +13,6 @@ from aqt.utils import saveGeom, restoreGeom, mungeQA,\
showWarning, openHelp, downArrow showWarning, openHelp, downArrow
from anki.utils import isMac, isWin, joinFields from anki.utils import isMac, isWin, joinFields
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
import anki.js
class CardLayout(QDialog): class CardLayout(QDialog):
@ -226,14 +224,17 @@ Please create a new card type first."""))
c = self.card c = self.card
ti = self.maybeTextInput ti = self.maybeTextInput
base = self.mw.baseHTML() 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'].frontWeb.setEnabled(False)
self.tab['pform'].backWeb.setEnabled(False) self.tab['pform'].backWeb.setEnabled(False)
self.tab['pform'].frontWeb.stdHtml( self.tab['pform'].frontWeb.stdHtml(
ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(), 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( self.tab['pform'].backWeb.stdHtml(
ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(), 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'].frontWeb.setEnabled(True)
self.tab['pform'].backWeb.setEnabled(True) self.tab['pform'].backWeb.setEnabled(True)
clearAudioQueue() clearAudioQueue()

View file

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

View file

@ -23,470 +23,13 @@ from aqt.webview import AnkiWebView
from aqt.utils import shortcut, showInfo, showWarning, getFile, \ from aqt.utils import shortcut, showInfo, showWarning, getFile, \
openHelp, tooltip, downArrow openHelp, tooltip, downArrow
import aqt import aqt
import anki.js
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp") pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga") audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
_html = """ _html = """
<style> <style>html { background: %s; }</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>
<div id="topbuts">%s</div> <div id="topbuts">%s</div>
<div id="fields"></div> <div id="fields"></div>
<div id="dupes" style="display:none;"><a href="#" onclick="pycmd('dupes');return false;">%s</a></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)) """ % dict(flds=_("Fields"), cards=_("Cards"), rightbts="".join(righttopbtns))
bgcol = self.mw.app.palette().window().color().name() bgcol = self.mw.app.palette().window().color().name()
# then load page # then load page
self.web.stdHtml(_html % ( html = self.web.bundledCSS("editor.css") + _html
self.web.stdHtml(html % (
bgcol, bgcol,
topbuts, topbuts,
_("Show Duplicates")), js=anki.js.jquery, head=self.mw.baseHTML()) _("Show Duplicates")), head=self.mw.baseHTML(),
js=["jquery.js", "editor.js"])
# Top buttons # Top buttons
###################################################################### ######################################################################

View file

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

View file

@ -5,6 +5,7 @@
from aqt.qt import * from aqt.qt import *
from http import HTTPStatus from http import HTTPStatus
import http.server import http.server
import socketserver
import errno import errno
# locate web folder in source/binary distribution # locate web folder in source/binary distribution
@ -22,6 +23,11 @@ def _getExportFolder():
_exportFolder = _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): class MediaServer(QThread):
def run(self): def run(self):
@ -29,7 +35,7 @@ class MediaServer(QThread):
self.server = None self.server = None
while not self.server: while not self.server:
try: try:
self.server = http.server.HTTPServer( self.server = ThreadedHTTPServer(
("localhost", self.port), RequestHandler) ("localhost", self.port), RequestHandler)
except OSError as e: except OSError as e:
if e.errno == errno.EADDRINUSE: if e.errno == errno.EADDRINUSE:

View file

@ -121,56 +121,6 @@ class Reviewer:
_revHtml = """ _revHtml = """
<img src="qrc:/icons/rating.png" id=star class=marked> <img src="qrc:/icons/rating.png" id=star class=marked>
<div id=qa></div> <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): def _initWeb(self):
@ -179,13 +129,20 @@ function _typeAnsPress() {
base = self.mw.baseHTML() base = self.mw.baseHTML()
# main window # main window
self.web.onLoadFinished = self._showQuestion 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 # show answer / ease buttons
self.bottom.web.show() self.bottom.web.show()
self.bottom.web.onLoadFinished = self._onBottomLoadFinished self.bottom.web.onLoadFinished = self._onBottomLoadFinished
self.bottom.web.stdHtml( self.bottom.web.stdHtml(
self._bottomHTML(), self._bottomHTML(),
self.bottom._css + self._bottomCSS) self.bottom._css + self._bottomCSS,
js=["jquery.js", "reviewer-bottom.js"]
)
# Showing the question # Showing the question
########################################################################## ##########################################################################
@ -573,46 +530,7 @@ padding: 3px;
</table> </table>
</center> </center>
<script> <script>
var time = %(time)d; 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();
}
</script> </script>
""" % dict(rem=self._remaining(), edit=_("Edit"), """ % dict(rem=self._remaining(), edit=_("Edit"),
editkey=_("Shortcut key: %s") % "E", editkey=_("Shortcut key: %s") % "E",

View file

@ -79,5 +79,6 @@ class DeckStats(QDialog):
stats = self.mw.col.stats() stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection stats.wholeCollection = self.wholeCollection
self.report = stats.report(type=self.period) 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() self.mw.progress.finish()

View file

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