Merge pull request #1292 from hgiesel/reviewerplus

Move reviewer to ts/reviewer v2
This commit is contained in:
Damien Elmes 2021-07-17 09:10:14 +10:00 committed by GitHub
commit ce5f9bb9b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 289 additions and 275 deletions

View File

@ -124,17 +124,14 @@ class Previewer(QDialog):
self._close_callback()
def _setup_web_view(self) -> None:
jsinc = [
"js/vendor/jquery.min.js",
"js/vendor/css_browser_selector.min.js",
"js/mathjax.js",
"js/vendor/mathjax/tex-chtml.js",
"js/reviewer.js",
]
self._web.stdHtml(
self.mw.reviewer.revHtml(),
css=["css/reviewer.css"],
js=jsinc,
js=[
"js/mathjax.js",
"js/vendor/mathjax/tex-chtml.js",
"js/reviewer.js",
],
context=self,
)
self._web.set_bridge_command(self._on_bridge_cmd, self)

View File

@ -330,17 +330,14 @@ class CardLayout(QDialog):
)
qconnect(pform.preview_settings.clicked, self.on_preview_settings)
jsinc = [
"js/vendor/jquery.min.js",
"js/vendor/css_browser_selector.min.js",
"js/mathjax.js",
"js/vendor/mathjax/tex-chtml.js",
"js/reviewer.js",
]
self.preview_web.stdHtml(
self.mw.reviewer.revHtml(),
css=["css/reviewer.css"],
js=jsinc,
js=[
"js/mathjax.js",
"js/vendor/mathjax/tex-chtml.js",
"js/reviewer.js",
],
context=self,
)
self.preview_web.set_bridge_command(self._on_bridge_cmd, self)

View File

@ -26,11 +26,20 @@ copy_files_into_group(
package = "//ts/editor",
)
copy_files_into_group(
name = "reviewer",
srcs = [
"reviewer.css",
],
package = "//ts/reviewer",
)
filegroup(
name = "css",
srcs = [
"css_local",
"editor",
"reviewer",
],
visibility = ["//qt:__subpackages__"],
)

View File

@ -38,9 +38,9 @@ copy_files_into_group(
)
copy_files_into_group(
name = "reviewer_extras",
name = "reviewer",
srcs = [
"reviewer_extras.js",
"reviewer.js",
],
package = "//ts/reviewer",
)
@ -50,8 +50,8 @@ filegroup(
srcs = [
"aqt_es5",
"editor",
"reviewer",
"mathjax.js",
"reviewer_extras",
"//qt/aqt/data/web/js/vendor",
],
visibility = ["//qt:__subpackages__"],

View File

@ -1,240 +0,0 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
declare var MathJax: any;
type Callback = () => void | Promise<void>;
var ankiPlatform = "desktop";
var typeans: HTMLElement | undefined;
var _updatingQueue: Promise<void> = Promise.resolve();
var onUpdateHook: Array<Callback>;
var onShownHook: Array<Callback>;
function _runHook(arr: Array<Callback>): Promise<void[]> {
const promises = [];
for (let i = 0; i < arr.length; i++) {
promises.push(arr[i]());
}
return Promise.all(promises);
}
function _queueAction(action: Callback): void {
_updatingQueue = _updatingQueue.then(action);
}
function setInnerHTML(element: Element, html: string): void {
for (const oldVideo of element.getElementsByTagName("video")) {
oldVideo.pause();
while (oldVideo.firstChild) {
oldVideo.removeChild(oldVideo.firstChild);
}
oldVideo.load();
}
element.innerHTML = html;
for (const oldScript of element.getElementsByTagName("script")) {
const newScript = document.createElement("script");
for (const attribute of oldScript.attributes) {
newScript.setAttribute(attribute.name, attribute.value);
}
newScript.appendChild(document.createTextNode(oldScript.innerHTML));
oldScript.parentNode.replaceChild(newScript, oldScript);
}
}
async function _updateQA(
html: string,
_unusused: unknown,
onupdate: Callback,
onshown: Callback
): Promise<void> {
onUpdateHook = [onupdate];
onShownHook = [onshown];
const qa = document.getElementById("qa")!;
const renderError =
(kind: string) =>
(error: Error): void => {
const errorMessage = String(error).substring(0, 2000);
const errorStack = String(error.stack).substring(0, 2000);
qa.innerHTML =
`Invalid ${kind} on card: ${errorMessage}\n${errorStack}`.replace(
/\n/g,
"<br>"
);
};
// hide current card
qa.style.opacity = "0";
// update card
try {
setInnerHTML(qa, html);
} catch (error) {
renderError("HTML")(error);
}
await _runHook(onUpdateHook);
// wait for mathjax to ready
await MathJax.startup.promise
.then(() => {
// clear MathJax buffers from previous typesets
MathJax.typesetClear();
return MathJax.typesetPromise([qa]);
})
.catch(renderError("MathJax"));
// and reveal card when processing is done
qa.style.opacity = "1";
await _runHook(onShownHook);
}
function _showQuestion(q: string, a: string, bodyclass: string): void {
_queueAction(() =>
_updateQA(
q,
null,
function () {
// return to top of window
window.scrollTo(0, 0);
document.body.className = bodyclass;
},
function () {
// focus typing area if visible
typeans = document.getElementById("typeans");
if (typeans) {
typeans.focus();
}
// preload images
allImagesLoaded().then(() => preloadAnswerImages(q, a));
}
)
);
}
function _showAnswer(a: string, bodyclass: string): void {
_queueAction(() =>
_updateQA(
a,
null,
function () {
if (bodyclass) {
// when previewing
document.body.className = bodyclass;
}
// avoid scrolling to the answer until images load
allImagesLoaded().then(scrollToAnswer);
},
function () {}
)
);
}
const _flagColours = {
1: "#e25252",
2: "#ffb347",
3: "#54c414",
4: "#578cff",
5: "#ff82ee",
6: "#00d1b5",
7: "#9649dd",
};
function _drawFlag(flag: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): void {
const elem = document.getElementById("_flag");
if (flag === 0) {
elem.setAttribute("hidden", "");
return;
}
elem.removeAttribute("hidden");
elem.style.color = _flagColours[flag];
}
function _drawMark(mark: boolean): void {
const elem = document.getElementById("_mark");
if (!mark) {
elem.setAttribute("hidden", "");
} else {
elem.removeAttribute("hidden");
}
}
function _typeAnsPress(): void {
const code = (window.event as KeyboardEvent).code;
if (["Enter", "NumpadEnter"].includes(code)) {
pycmd("ans");
}
}
function _emulateMobile(enabled: boolean): void {
const list = document.documentElement.classList;
if (enabled) {
list.add("mobile");
} else {
list.remove("mobile");
}
}
function allImagesLoaded(): Promise<void[]> {
return Promise.all(
Array.from(document.getElementsByTagName("img")).map(imageLoaded)
);
}
function imageLoaded(img: HTMLImageElement): Promise<void> {
return img.complete
? Promise.resolve()
: new Promise((resolve) => {
img.addEventListener("load", () => resolve());
img.addEventListener("error", () => resolve());
});
}
function scrollToAnswer(): void {
document.getElementById("answer")?.scrollIntoView();
}
function injectPreloadLink(href: string, as: string): void {
const link = document.createElement("link");
link.rel = "preload";
link.href = href;
link.as = as;
document.head.appendChild(link);
}
function clearPreloadLinks(): void {
document.head
.querySelectorAll("link[rel='preload']")
.forEach((link) => link.remove());
}
function extractImageSrcs(html: string): string[] {
const fragment = document.createRange().createContextualFragment(html);
const srcs = [...fragment.querySelectorAll("img[src]")].map(
(img) => (img as HTMLImageElement).src
);
return srcs;
}
function preloadAnswerImages(qHtml: string, aHtml: string): void {
clearPreloadLinks();
const aSrcs = extractImageSrcs(aHtml);
if (aSrcs.length) {
const qSrcs = extractImageSrcs(qHtml);
const diff = aSrcs.filter((src) => !qSrcs.includes(src));
diff.forEach((src) => injectPreloadLink(src, "image"));
}
}

View File

@ -288,12 +288,9 @@ class Reviewer:
self.revHtml(),
css=["css/reviewer.css"],
js=[
"js/vendor/jquery.min.js",
"js/vendor/css_browser_selector.min.js",
"js/mathjax.js",
"js/vendor/mathjax/tex-chtml.js",
"js/reviewer.js",
"js/reviewer_extras.js",
],
context=self,
)
@ -699,7 +696,7 @@ class Reviewer:
return s
def _getTypedAnswer(self) -> None:
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
self.web.evalWithCallback("getTypedAnswer();", self._onTypedAnswer)
def _onTypedAnswer(self, val: None) -> None:
self.typedAnswer = val or ""

View File

@ -33,6 +33,7 @@ compile_sass(
visibility = ["//visibility:public"],
deps = [
"//ts/sass:scrollbar_lib",
"//ts/sass/codemirror",
],
)
@ -132,10 +133,10 @@ esbuild(
output_css = "editor.css",
visibility = ["//visibility:public"],
deps = [
"base_css",
"bootstrap-icons",
"editor_ts",
"base_css",
"local_css",
"bootstrap-icons",
"mdi-icons",
"svelte_components",
"//ts/components",

View File

@ -5,23 +5,39 @@ load("//ts/svelte:svelte.bzl", "svelte", "svelte_check")
load("//ts:esbuild.bzl", "esbuild")
load("//ts:compile_sass.bzl", "compile_sass")
compile_sass(
srcs = [
"reviewer.scss",
],
group = "reviewer_scss",
visibility = ["//visibility:public"],
deps = [],
)
ts_library(
name = "lib",
name = "reviewer_ts",
srcs = glob(["*.ts"]),
deps = ["//ts/lib"],
deps = [
"//ts/lib",
"//ts/lib:backend_proto",
"@npm//css-browser-selector",
"@npm//jquery",
],
)
esbuild(
name = "reviewer_extras",
name = "reviewer",
args = [
"--resolve-extensions=.mjs,.js",
"--log-level=warning",
],
entry_point = "index.ts",
entry_point = "index_wrapper.ts",
visibility = ["//visibility:public"],
deps = [
":lib",
":reviewer_ts",
"//ts/lib",
"//ts/lib:backend_proto",
"@npm//protobufjs",
],
)

49
ts/reviewer/images.ts Normal file
View File

@ -0,0 +1,49 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function injectPreloadLink(href: string, as: string): void {
const link = document.createElement("link");
link.rel = "preload";
link.href = href;
link.as = as;
document.head.appendChild(link);
}
export function allImagesLoaded(): Promise<void[]> {
return Promise.all(
Array.from(document.getElementsByTagName("img")).map(imageLoaded)
);
}
function imageLoaded(img: HTMLImageElement): Promise<void> {
return img.complete
? Promise.resolve()
: new Promise((resolve) => {
img.addEventListener("load", () => resolve());
img.addEventListener("error", () => resolve());
});
}
function clearPreloadLinks(): void {
document.head
.querySelectorAll("link[rel='preload']")
.forEach((link) => link.remove());
}
function extractImageSrcs(html: string): string[] {
const fragment = document.createRange().createContextualFragment(html);
const srcs = [...fragment.querySelectorAll("img[src]")].map(
(img) => (img as HTMLImageElement).src
);
return srcs;
}
export function preloadAnswerImages(qHtml: string, aHtml: string): void {
clearPreloadLinks();
const aSrcs = extractImageSrcs(aHtml);
if (aSrcs.length) {
const qSrcs = extractImageSrcs(qHtml);
const diff = aSrcs.filter((src) => !qSrcs.includes(src));
diff.forEach((src) => injectPreloadLink(src, "image"));
}
}

View File

@ -1,9 +1,189 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// This is a temporary extra file we load separately from reviewer.ts. Once
// reviewer.ts has been migrated into ts/, the code here can be merged into
// it.
/* eslint
@typescript-eslint/no-non-null-assertion: "off",
@typescript-eslint/no-explicit-any: "off",
*/
import { mutateNextCardStates } from "./answering";
globalThis.anki = { ...globalThis.anki, mutateNextCardStates };
import "css-browser-selector/css_browser_selector";
import "jquery/dist/jquery";
export { mutateNextCardStates } from "./answering";
import { bridgeCommand } from "lib/bridgecommand";
import { allImagesLoaded, preloadAnswerImages } from "./images";
declare const MathJax: any;
type Callback = () => void | Promise<void>;
export const onUpdateHook: Array<Callback> = [];
export const onShownHook: Array<Callback> = [];
export const ankiPlatform = "desktop";
let typeans: HTMLInputElement | undefined;
export function getTypedAnswer(): string | null {
return typeans?.value ?? null;
}
function _runHook(arr: Array<Callback>): Promise<void[]> {
const promises: (Promise<void> | void)[] = [];
for (let i = 0; i < arr.length; i++) {
promises.push(arr[i]());
}
return Promise.all(promises);
}
let _updatingQueue: Promise<void> = Promise.resolve();
function _queueAction(action: Callback): void {
_updatingQueue = _updatingQueue.then(action);
}
function setInnerHTML(element: Element, html: string): void {
for (const oldVideo of element.getElementsByTagName("video")) {
oldVideo.pause();
while (oldVideo.firstChild) {
oldVideo.removeChild(oldVideo.firstChild);
}
oldVideo.load();
}
element.innerHTML = html;
for (const oldScript of element.getElementsByTagName("script")) {
const newScript = document.createElement("script");
for (const attribute of oldScript.attributes) {
newScript.setAttribute(attribute.name, attribute.value);
}
newScript.appendChild(document.createTextNode(oldScript.innerHTML));
oldScript.parentNode!.replaceChild(newScript, oldScript);
}
}
const renderError =
(type: string) =>
(error: Error): string => {
const errorMessage = String(error).substring(0, 2000);
const errorStack = String(error.stack).substring(0, 2000);
return `<div>Invalid ${type} on card: ${errorMessage}\n${errorStack}</div>`.replace(
/\n/g,
"<br>"
);
};
async function _updateQA(
html: string,
_unusused: unknown,
onupdate: Callback,
onshown: Callback
): Promise<void> {
onUpdateHook.length = 0;
onUpdateHook.push(onupdate);
onShownHook.length = 0;
onShownHook.push(onshown);
const qa = document.getElementById("qa")!;
qa.style.opacity = "0";
try {
setInnerHTML(qa, html);
} catch (error) {
setInnerHTML(qa, renderError("html")(error));
}
await _runHook(onUpdateHook);
// wait for mathjax to ready
await MathJax.startup.promise
.then(() => {
// clear MathJax buffers from previous typesets
MathJax.typesetClear();
return MathJax.typesetPromise([qa]);
})
.catch(renderError("MathJax"));
qa.style.opacity = "1";
await _runHook(onShownHook);
}
export function _showQuestion(q: string, a: string, bodyclass: string): void {
_queueAction(() =>
_updateQA(
q,
null,
function () {
// return to top of window
window.scrollTo(0, 0);
document.body.className = bodyclass;
},
function () {
// focus typing area if visible
typeans = document.getElementById("typeans") as HTMLInputElement;
if (typeans) {
typeans.focus();
}
// preload images
allImagesLoaded().then(() => preloadAnswerImages(q, a));
}
)
);
}
function scrollToAnswer(): void {
document.getElementById("answer")?.scrollIntoView();
}
export function _showAnswer(a: string, bodyclass: string): void {
_queueAction(() =>
_updateQA(
a,
null,
function () {
if (bodyclass) {
// when previewing
document.body.className = bodyclass;
}
// avoid scrolling to the answer until images load
allImagesLoaded().then(scrollToAnswer);
},
function () {
/* noop */
}
)
);
}
export function _drawFlag(flag: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): void {
const elem = document.getElementById("_flag")!;
elem.toggleAttribute("hidden", flag === 0);
elem.style.color = `var(--flag${flag}-fg)`;
}
export function _drawMark(mark: boolean): void {
document.getElementById("_mark")!.toggleAttribute("hidden", !mark);
}
export function _typeAnsPress(): void {
const code = (window.event as KeyboardEvent).code;
if (["Enter", "NumpadEnter"].includes(code)) {
bridgeCommand("ans");
}
}
export function _emulateMobile(enabled: boolean): void {
document.documentElement.classList.toggle("mobile", enabled);
}

View File

@ -0,0 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// extend the global namespace with our exports - not sure if there's a better way with esbuild
import * as globals from "./index";
for (const key in globals) {
window[key] = globals[key];
}