mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 00:12:25 -04:00
experimental prewrap support
- add option to wrap html in implicit pre-wrap environment during editing and review - defaults to off - update paste filter to convert divs/Ps to newlines and non-breaking spaces to normal ones - catch enter key and write \n instead of creating a new div also: - remove extra caretToEnd() call that is no longer required - add dd/dt/dl to allowed tags
This commit is contained in:
parent
f7b3457ff0
commit
17bb179d06
4 changed files with 135 additions and 14 deletions
|
@ -135,6 +135,10 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
|
||||||
else:
|
else:
|
||||||
args = tuple()
|
args = tuple()
|
||||||
self._qa = self.col._renderQA(data, *args)
|
self._qa = self.col._renderQA(data, *args)
|
||||||
|
if m.get("prewrap", False):
|
||||||
|
wsdiv = "<div style='white-space:pre-wrap;'>{}</div>"
|
||||||
|
self._qa['q'] = wsdiv.format(self._qa['q'])
|
||||||
|
self._qa['a'] = wsdiv.format(self._qa['a'])
|
||||||
return self._qa
|
return self._qa
|
||||||
|
|
||||||
def note(self, reload=False):
|
def note(self, reload=False):
|
||||||
|
|
106
aqt/editor.py
106
aqt/editor.py
|
@ -61,6 +61,8 @@ background-image: -webkit-gradient(linear, left top, left bottom,
|
||||||
border-bottom: 3px solid #000;
|
border-bottom: 3px solid #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prewrap { white-space: pre-wrap; }
|
||||||
|
|
||||||
#fields { margin-top: 35px; }
|
#fields { margin-top: 35px; }
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,6 +71,7 @@ background-image: -webkit-gradient(linear, left top, left bottom,
|
||||||
var currentField = null;
|
var currentField = null;
|
||||||
var changeTimer = null;
|
var changeTimer = null;
|
||||||
var dropTarget = null;
|
var dropTarget = null;
|
||||||
|
var prewrapMode = false;
|
||||||
|
|
||||||
String.prototype.format = function() {
|
String.prototype.format = function() {
|
||||||
var args = arguments;
|
var args = arguments;
|
||||||
|
@ -93,6 +96,12 @@ function onKey() {
|
||||||
currentField.blur();
|
currentField.blur();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// catch enter key in prewrap mode
|
||||||
|
if (window.event.which == 13 && prewrapMode) {
|
||||||
|
window.event.preventDefault();
|
||||||
|
insertNewline();
|
||||||
|
return;
|
||||||
|
}
|
||||||
clearChangeTimer();
|
clearChangeTimer();
|
||||||
changeTimer = setTimeout(function () {
|
changeTimer = setTimeout(function () {
|
||||||
updateButtonState();
|
updateButtonState();
|
||||||
|
@ -100,6 +109,40 @@ function onKey() {
|
||||||
}, 600);
|
}, 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() {
|
function checkForEmptyField() {
|
||||||
if (currentField.innerHTML == "") {
|
if (currentField.innerHTML == "") {
|
||||||
currentField.innerHTML = "<br>";
|
currentField.innerHTML = "<br>";
|
||||||
|
@ -152,8 +195,6 @@ function onFocus(elem) {
|
||||||
if (mouseDown) { return; }
|
if (mouseDown) { return; }
|
||||||
// do this twice so that there's no flicker on newer versions
|
// do this twice so that there's no flicker on newer versions
|
||||||
caretToEnd();
|
caretToEnd();
|
||||||
// need to do this in a timeout for older qt versions
|
|
||||||
setTimeout(function () { caretToEnd() }, 1);
|
|
||||||
// scroll if bottom of element off the screen
|
// scroll if bottom of element off the screen
|
||||||
function pos(obj) {
|
function pos(obj) {
|
||||||
var cur = 0;
|
var cur = 0;
|
||||||
|
@ -251,7 +292,7 @@ function wrap(front, back) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function setFields(fields, focusTo) {
|
function setFields(fields, focusTo, prewrap) {
|
||||||
var txt = "";
|
var txt = "";
|
||||||
for (var i=0; i<fields.length; i++) {
|
for (var i=0; i<fields.length; i++) {
|
||||||
var n = fields[i][0];
|
var n = fields[i][0];
|
||||||
|
@ -274,6 +315,10 @@ function setFields(fields, focusTo) {
|
||||||
$("#f"+focusTo).focus();
|
$("#f"+focusTo).focus();
|
||||||
}
|
}
|
||||||
maybeDisableButtons();
|
maybeDisableButtons();
|
||||||
|
prewrapMode = prewrap;
|
||||||
|
if (prewrap) {
|
||||||
|
$(".field").addClass("prewrap");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function setBackgrounds(cols) {
|
function setBackgrounds(cols) {
|
||||||
|
@ -319,7 +364,7 @@ var allowedTags = {};
|
||||||
|
|
||||||
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
|
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
|
||||||
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
|
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
|
||||||
"STRONG", "PRE", "SUB", "SUP", "TABLE"];
|
"STRONG", "PRE", "SUB", "SUP", "TABLE", "DD", "DT", "DL"];
|
||||||
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
|
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
|
||||||
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
|
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
|
||||||
}
|
}
|
||||||
|
@ -330,9 +375,41 @@ allowedTags["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
||||||
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
||||||
allowedTags["IMG"] = {"attrs": ["SRC"]};
|
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) {
|
var filterNode = function(node) {
|
||||||
// if it's a text node, nothing to do
|
// text node?
|
||||||
if (node.nodeType == 3) {
|
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(/ |\u00a0/g, " ");
|
||||||
|
|
||||||
|
node.nodeValue = val;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,7 +430,17 @@ var filterNode = function(node) {
|
||||||
|
|
||||||
var tag = allowedTags[node.tagName];
|
var tag = allowedTags[node.tagName];
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
|
if (!node.innerHTML) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
} else {
|
||||||
node.outerHTML = node.innerHTML;
|
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 {
|
} else {
|
||||||
// allowed, filter out attributes
|
// allowed, filter out attributes
|
||||||
var toRemove = [];
|
var toRemove = [];
|
||||||
|
@ -644,8 +731,8 @@ class Editor:
|
||||||
data = []
|
data = []
|
||||||
for fld, val in list(self.note.items()):
|
for fld, val in list(self.note.items()):
|
||||||
data.append((fld, self.mw.col.media.escapeImages(val)))
|
data.append((fld, self.mw.col.media.escapeImages(val)))
|
||||||
self.web.eval("setFields(%s, %d);" % (
|
self.web.eval("setFields(%s, %d, %s);" % (
|
||||||
json.dumps(data), field))
|
json.dumps(data), field, json.dumps(self.prewrapMode())))
|
||||||
self.web.eval("setFonts(%s);" % (
|
self.web.eval("setFonts(%s);" % (
|
||||||
json.dumps(self.fonts())))
|
json.dumps(self.fonts())))
|
||||||
self.checkValid()
|
self.checkValid()
|
||||||
|
@ -655,6 +742,8 @@ class Editor:
|
||||||
self.web.setFocus()
|
self.web.setFocus()
|
||||||
self.stealFocus = False
|
self.stealFocus = False
|
||||||
|
|
||||||
|
def prewrapMode(self):
|
||||||
|
return self.note.model().get('prewrap', False)
|
||||||
|
|
||||||
def focus(self):
|
def focus(self):
|
||||||
self.web.setFocus()
|
self.web.setFocus()
|
||||||
|
@ -962,6 +1051,7 @@ to a cloze type first, via Edit>Change Note Type."""))
|
||||||
for node in doc(tag):
|
for node in doc(tag):
|
||||||
node.decompose()
|
node.decompose()
|
||||||
|
|
||||||
|
if not self.prewrapMode():
|
||||||
# convert p tags to divs
|
# convert p tags to divs
|
||||||
for node in doc("p"):
|
for node in doc("p"):
|
||||||
node.name = "div"
|
node.name = "div"
|
||||||
|
@ -1123,8 +1213,6 @@ class EditorWebView(AnkiWebView):
|
||||||
|
|
||||||
# normal text; convert it to HTML
|
# normal text; convert it to HTML
|
||||||
txt = html.escape(txt)
|
txt = html.escape(txt)
|
||||||
txt = txt.replace("\n", "<br>")
|
|
||||||
txt = txt.replace(" ", " ")
|
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def _processHtml(self, mime):
|
def _processHtml(self, mime):
|
||||||
|
|
|
@ -109,6 +109,7 @@ class Models(QDialog):
|
||||||
frm.setupUi(d)
|
frm.setupUi(d)
|
||||||
frm.latexHeader.setText(self.model['latexPre'])
|
frm.latexHeader.setText(self.model['latexPre'])
|
||||||
frm.latexFooter.setText(self.model['latexPost'])
|
frm.latexFooter.setText(self.model['latexPost'])
|
||||||
|
frm.newStyleWhitespace.setChecked(self.model.get("prewrap", False))
|
||||||
d.setWindowTitle(_("Options for %s") % self.model['name'])
|
d.setWindowTitle(_("Options for %s") % self.model['name'])
|
||||||
frm.buttonBox.helpRequested.connect(lambda: openHelp("latex"))
|
frm.buttonBox.helpRequested.connect(lambda: openHelp("latex"))
|
||||||
restoreGeom(d, "modelopts")
|
restoreGeom(d, "modelopts")
|
||||||
|
@ -116,6 +117,7 @@ class Models(QDialog):
|
||||||
saveGeom(d, "modelopts")
|
saveGeom(d, "modelopts")
|
||||||
self.model['latexPre'] = str(frm.latexHeader.toPlainText())
|
self.model['latexPre'] = str(frm.latexHeader.toPlainText())
|
||||||
self.model['latexPost'] = str(frm.latexFooter.toPlainText())
|
self.model['latexPost'] = str(frm.latexFooter.toPlainText())
|
||||||
|
self.model['prewrap'] = frm.newStyleWhitespace.isChecked()
|
||||||
|
|
||||||
def saveModel(self):
|
def saveModel(self):
|
||||||
self.mm.save(self.model)
|
self.mm.save(self.model)
|
||||||
|
|
|
@ -57,6 +57,33 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_2">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>General</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="newStyleWhitespace">
|
||||||
|
<property name="text">
|
||||||
|
<string>New style whitespace handling (EXPERIMENTAL)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|
Loading…
Reference in a new issue