Add a preference to toggle LaTeX generation (#3218)
* Add a preference to toggle LaTeX generation * Fix test * Remove LaTeX security restrictions * Show existing LaTeX images regardless of preference * Lift config check out of loop (dae) * Shift option to review settings; display warning when disabled (dae)
This commit is contained in:
parent
d981a6e3c6
commit
06f7aa393d
@ -13,6 +13,8 @@ preferences-media-is-not-backed-up = Media is not backed up. Please create a per
|
|||||||
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
|
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
|
||||||
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
|
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
|
||||||
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
|
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
|
||||||
|
preferences-generate-latex-images-automatically = Generate LaTeX images (security risk)
|
||||||
|
preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences.
|
||||||
preferences-periodically-sync-media = Periodically sync media
|
preferences-periodically-sync-media = Periodically sync media
|
||||||
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
|
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
|
||||||
preferences-preferences = Preferences
|
preferences-preferences = Preferences
|
||||||
|
@ -53,6 +53,7 @@ message ConfigKey {
|
|||||||
RESET_COUNTS_REVIEWER = 22;
|
RESET_COUNTS_REVIEWER = 22;
|
||||||
RANDOM_ORDER_REPOSITION = 23;
|
RANDOM_ORDER_REPOSITION = 23;
|
||||||
SHIFT_POSITION_OF_EXISTING_CARDS = 24;
|
SHIFT_POSITION_OF_EXISTING_CARDS = 24;
|
||||||
|
RENDER_LATEX = 25;
|
||||||
}
|
}
|
||||||
enum String {
|
enum String {
|
||||||
SET_DUE_BROWSER = 0;
|
SET_DUE_BROWSER = 0;
|
||||||
@ -121,6 +122,7 @@ message Preferences {
|
|||||||
bool paste_strips_formatting = 3;
|
bool paste_strips_formatting = 3;
|
||||||
string default_search_text = 4;
|
string default_search_text = 4;
|
||||||
bool ignore_accents_in_search = 5;
|
bool ignore_accents_in_search = 5;
|
||||||
|
bool render_latex = 6;
|
||||||
}
|
}
|
||||||
message BackupLimits {
|
message BackupLimits {
|
||||||
uint32 daily = 1;
|
uint32 daily = 1;
|
||||||
|
@ -5,12 +5,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import html
|
import html
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.collection
|
import anki.collection
|
||||||
from anki import card_rendering_pb2, hooks
|
from anki import card_rendering_pb2, hooks
|
||||||
|
from anki.config import Config
|
||||||
from anki.models import NotetypeDict
|
from anki.models import NotetypeDict
|
||||||
from anki.template import TemplateRenderContext, TemplateRenderOutput
|
from anki.template import TemplateRenderContext, TemplateRenderOutput
|
||||||
from anki.utils import call, is_mac, namedtmp, tmpdir
|
from anki.utils import call, is_mac, namedtmp, tmpdir
|
||||||
@ -36,9 +36,6 @@ svgCommands = [
|
|||||||
["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"],
|
["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"],
|
||||||
]
|
]
|
||||||
|
|
||||||
# if off, use existing media but don't create new
|
|
||||||
build = True # pylint: disable=invalid-name
|
|
||||||
|
|
||||||
# add standard tex install location to osx
|
# add standard tex install location to osx
|
||||||
if is_mac:
|
if is_mac:
|
||||||
os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin"
|
os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin"
|
||||||
@ -104,11 +101,15 @@ def render_latex_returning_errors(
|
|||||||
out = ExtractedLatexOutput.from_proto(proto)
|
out = ExtractedLatexOutput.from_proto(proto)
|
||||||
errors = []
|
errors = []
|
||||||
html = out.html
|
html = out.html
|
||||||
|
render_latex = col.get_config_bool(Config.Bool.RENDER_LATEX)
|
||||||
|
|
||||||
for latex in out.latex:
|
for latex in out.latex:
|
||||||
# don't need to render?
|
# don't need to render?
|
||||||
if not build or col.media.have(latex.filename):
|
if col.media.have(latex.filename):
|
||||||
continue
|
continue
|
||||||
|
if not render_latex:
|
||||||
|
errors.append(col.tr.preferences_latex_generation_disabled())
|
||||||
|
return html, errors
|
||||||
|
|
||||||
err = _save_latex_image(col, latex, header, footer, svg)
|
err = _save_latex_image(col, latex, header, footer, svg)
|
||||||
if err is not None:
|
if err is not None:
|
||||||
@ -126,24 +127,6 @@ def _save_latex_image(
|
|||||||
) -> str | None:
|
) -> str | None:
|
||||||
# add header/footer
|
# add header/footer
|
||||||
latex = f"{header}\n{extracted.latex_body}\n{footer}"
|
latex = f"{header}\n{extracted.latex_body}\n{footer}"
|
||||||
# it's only really secure if run in a jail, but these are the most common
|
|
||||||
tmplatex = latex.replace("\\includegraphics", "")
|
|
||||||
for bad in (
|
|
||||||
"\\write18",
|
|
||||||
"\\readline",
|
|
||||||
"\\input",
|
|
||||||
"\\include",
|
|
||||||
"\\catcode",
|
|
||||||
"\\openout",
|
|
||||||
"\\write",
|
|
||||||
"\\loop",
|
|
||||||
"\\def",
|
|
||||||
"\\shipout",
|
|
||||||
):
|
|
||||||
# don't mind if the sequence is only part of a command
|
|
||||||
bad_re = f"\\{bad}[^a-zA-Z]"
|
|
||||||
if re.search(bad_re, tmplatex):
|
|
||||||
return col.tr.media_for_security_reasons_is_not(val=bad)
|
|
||||||
|
|
||||||
# commands to use
|
# commands to use
|
||||||
if svg:
|
if svg:
|
||||||
|
@ -6,12 +6,14 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from anki.config import Config
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from tests.shared import getEmptyCol
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_latex():
|
def test_latex():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
|
col.set_config_bool(Config.Bool.RENDER_LATEX, True)
|
||||||
# change latex cmd to simulate broken build
|
# change latex cmd to simulate broken build
|
||||||
import anki.latex
|
import anki.latex
|
||||||
|
|
||||||
@ -51,49 +53,9 @@ def test_latex():
|
|||||||
assert ".png" in oldcard.question()
|
assert ".png" in oldcard.question()
|
||||||
# if we turn off building, then previous cards should work, but cards with
|
# if we turn off building, then previous cards should work, but cards with
|
||||||
# missing media will show a broken image
|
# missing media will show a broken image
|
||||||
anki.latex.build = False
|
col.set_config_bool(Config.Bool.RENDER_LATEX, False)
|
||||||
note = col.newNote()
|
note = col.newNote()
|
||||||
note["Front"] = "[latex]foo[/latex]"
|
note["Front"] = "[latex]foo[/latex]"
|
||||||
col.addNote(note)
|
col.addNote(note)
|
||||||
assert len(os.listdir(col.media.dir())) == 2
|
assert len(os.listdir(col.media.dir())) == 2
|
||||||
assert ".png" in oldcard.question()
|
assert ".png" in oldcard.question()
|
||||||
# turn it on again so other test don't suffer
|
|
||||||
anki.latex.build = True
|
|
||||||
|
|
||||||
# bad commands
|
|
||||||
(result, msg) = _test_includes_bad_command("\\write18")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\readline")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\input")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\include")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\catcode")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\openout")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\write")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\loop")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\def")
|
|
||||||
assert result, msg
|
|
||||||
(result, msg) = _test_includes_bad_command("\\shipout")
|
|
||||||
assert result, msg
|
|
||||||
|
|
||||||
# inserting commands beginning with a bad name should not raise an error
|
|
||||||
(result, msg) = _test_includes_bad_command("\\defeq")
|
|
||||||
assert not result, msg
|
|
||||||
# normal commands should not either
|
|
||||||
(result, msg) = _test_includes_bad_command("\\emph")
|
|
||||||
assert not result, msg
|
|
||||||
|
|
||||||
|
|
||||||
def _test_includes_bad_command(bad):
|
|
||||||
col = getEmptyCol()
|
|
||||||
note = col.newNote()
|
|
||||||
note["Front"] = f"[latex]{bad}[/latex]"
|
|
||||||
col.addNote(note)
|
|
||||||
q = without_unicode_isolation(note.cards()[0].question())
|
|
||||||
return (f"'{bad}' is not allowed on cards" in q, f"Card content: {q}")
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>606</width>
|
<width>636</width>
|
||||||
<height>638</height>
|
<height>638</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -347,7 +347,7 @@
|
|||||||
<property name="title">
|
<property name="title">
|
||||||
<string>preferences_review</string>
|
<string>preferences_review</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_16">
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="showPlayButtons">
|
<widget class="QCheckBox" name="showPlayButtons">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -413,6 +413,19 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="render_latex">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>preferences_generate_latex_images_automatically</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -1098,6 +1111,7 @@
|
|||||||
<tabstop>showProgress</tabstop>
|
<tabstop>showProgress</tabstop>
|
||||||
<tabstop>showEstimates</tabstop>
|
<tabstop>showEstimates</tabstop>
|
||||||
<tabstop>spacebar_rates_card</tabstop>
|
<tabstop>spacebar_rates_card</tabstop>
|
||||||
|
<tabstop>render_latex</tabstop>
|
||||||
<tabstop>pastePNG</tabstop>
|
<tabstop>pastePNG</tabstop>
|
||||||
<tabstop>paste_strips_formatting</tabstop>
|
<tabstop>paste_strips_formatting</tabstop>
|
||||||
<tabstop>useCurrent</tabstop>
|
<tabstop>useCurrent</tabstop>
|
||||||
|
@ -126,6 +126,7 @@ class Preferences(QDialog):
|
|||||||
form.paste_strips_formatting.setChecked(editing.paste_strips_formatting)
|
form.paste_strips_formatting.setChecked(editing.paste_strips_formatting)
|
||||||
form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search)
|
form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search)
|
||||||
form.pastePNG.setChecked(editing.paste_images_as_png)
|
form.pastePNG.setChecked(editing.paste_images_as_png)
|
||||||
|
form.render_latex.setChecked(editing.render_latex)
|
||||||
form.default_search_text.setText(editing.default_search_text)
|
form.default_search_text.setText(editing.default_search_text)
|
||||||
|
|
||||||
form.backup_explanation.setText(
|
form.backup_explanation.setText(
|
||||||
@ -154,6 +155,7 @@ class Preferences(QDialog):
|
|||||||
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
|
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
|
||||||
editing.paste_images_as_png = self.form.pastePNG.isChecked()
|
editing.paste_images_as_png = self.form.pastePNG.isChecked()
|
||||||
editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked()
|
editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked()
|
||||||
|
editing.render_latex = self.form.render_latex.isChecked()
|
||||||
editing.default_search_text = self.form.default_search_text.text()
|
editing.default_search_text = self.form.default_search_text.text()
|
||||||
editing.ignore_accents_in_search = (
|
editing.ignore_accents_in_search = (
|
||||||
self.form.ignore_accents_in_search.isChecked()
|
self.form.ignore_accents_in_search.isChecked()
|
||||||
|
@ -36,6 +36,7 @@ impl From<BoolKeyProto> for BoolKey {
|
|||||||
BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
|
BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
|
||||||
BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition,
|
BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition,
|
||||||
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
|
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
|
||||||
|
BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ pub enum BoolKey {
|
|||||||
NewCardsIgnoreReviewLimit,
|
NewCardsIgnoreReviewLimit,
|
||||||
PasteImagesAsPng,
|
PasteImagesAsPng,
|
||||||
PasteStripsFormatting,
|
PasteStripsFormatting,
|
||||||
|
RenderLatex,
|
||||||
PreviewBothSides,
|
PreviewBothSides,
|
||||||
RestorePositionBrowser,
|
RestorePositionBrowser,
|
||||||
RestorePositionReviewer,
|
RestorePositionReviewer,
|
||||||
|
@ -128,6 +128,7 @@ impl Collection {
|
|||||||
paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting),
|
paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting),
|
||||||
default_search_text: self.get_config_string(StringKey::DefaultSearchText),
|
default_search_text: self.get_config_string(StringKey::DefaultSearchText),
|
||||||
ignore_accents_in_search: self.get_config_bool(BoolKey::IgnoreAccentsInSearch),
|
ignore_accents_in_search: self.get_config_bool(BoolKey::IgnoreAccentsInSearch),
|
||||||
|
render_latex: self.get_config_bool(BoolKey::RenderLatex),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,6 +142,7 @@ impl Collection {
|
|||||||
self.set_config_bool_inner(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?;
|
self.set_config_bool_inner(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?;
|
||||||
self.set_config_string_inner(StringKey::DefaultSearchText, &s.default_search_text)?;
|
self.set_config_string_inner(StringKey::DefaultSearchText, &s.default_search_text)?;
|
||||||
self.set_config_bool_inner(BoolKey::IgnoreAccentsInSearch, s.ignore_accents_in_search)?;
|
self.set_config_bool_inner(BoolKey::IgnoreAccentsInSearch, s.ignore_accents_in_search)?;
|
||||||
|
self.set_config_bool_inner(BoolKey::RenderLatex, s.render_latex)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user