- Congratulations! You have successfully installed the Thunderbird LaTeX
- extension. Please take a few seconds to check that the settings we have
- found are correct.
-
Required software
-
- This extension depends on a regular LaTeX distribution and the dvipng
- software. If you haven't installed them already, here are a few tips. If
- you are running Linux, both can be easily installed through your
- distribution's package manager. If running MacOS, MacTeX is probably what you're
- looking for. If running Windows, MiKTeX is known to work.
-
-
- If you choose to install this software now, please make sure you click
- "autodetect again" at the bottom of the page once you're done with the
- setup.
-
-
-
Attention!
-
- It appears you are running Microsoft Windows.
-
-
- Other software is available here:
-
-
MiKTeX includes both
- latex and dvipng executables.
-
-
-
Where is the required software on Windows?
-
- If autodetection failed, latex.exe and dvipng.exe are
- usually found in the miktex\bin folder of your MiKTeX install
- directory.
-
-
-
Autodetection results
-
- We have found the required utilities to be in the following locations:
-
-
-
LaTeX:
-
dvipng:
-
-
-
Are these settings correct?
-
-
-
-
- Autodetect again
-
-
-
diff --git a/content/firstrun.js b/content/firstrun.js
deleted file mode 100644
index cb0fbb8..0000000
--- a/content/firstrun.js
+++ /dev/null
@@ -1,20 +0,0 @@
-(function () {
- function on_load() {
- var prefs = Components.classes["@mozilla.org/preferences-service;1"]
- .getService(Components.interfaces.nsIPrefService)
- .getBranch("tblatex.");
- if (prefs.getIntPref("firstrun") == 3)
- return;
-
- var tabmail = document.getElementById("tabmail");
- if (tabmail && 'openTab' in tabmail) /* Took this from Personas code ("Browse gallery"...) */
- Components.classes['@mozilla.org/appshell/window-mediator;1'].
- getService(Components.interfaces.nsIWindowMediator).
- getMostRecentWindow("mail:3pane").
- document.getElementById("tabmail").
- openTab("contentTab", { contentPage: "chrome://tblatex/content/firstrun.html" });
- else
- openDialog("chrome://tblatex/content/firstrun.html", "", "width=640,height=480");
- };
- window.addEventListener("load", on_load, false);
-})();
diff --git a/content/help.html b/content/help.html
deleted file mode 100644
index dcbc55a..0000000
--- a/content/help.html
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
- Latex It! help
-
-
-
-
- More about templates
-
-
- The default template gives LaTeX expressions the usual, familiar look of
- CM fonts. It is as minimal as possible to ensure that it works on every
- platform.
-
-
-\documentclass{article}
-\usepackage[utf8]{inputenc}
-\usepackage[active,displaymath,textmath]{preview} % DO NOT DELETE - this is required for baseline alignment
-\pagestyle{empty}
-\begin{document}
-__REPLACE_ME__ % this is where your LaTeX expression goes between $$
-\end{document}
-
-
- To insert LaTeX, we convert dvi to png directly. In the
- process the PNG file is cropped to a small image
- that only contains the formula. The \pagestyle{empty} line removes
- extra output such as page numbers, which is why cropping works.
-
-
- The __REPLACE_ME__ word will be replaced with the LaTeX expression
- you specified. Therefore, it is important you put it in the right place.
-
-
- The file is written as UTF-8 (always) so please do not remove the
- utf8 encoding.
-
-
Alternative templates
-
- This one uses the "Palatino" font, and includes several AMS
- packages to make it easier to insert symbols and specific features. The
- mathrsfs package allows one to use \mathscr{} to
- produce English capitals.
-
-
-\documentclass{article}
-\usepackage[utf8]{inputenc}
-\usepackage[active,displaymath,textmath]{preview} % DO NOT DELETE - this is required for baseline alignment
-\usepackage{amsmath,amssymb,amsopn}
-\usepackage{mathrsfs}
-\usepackage{palatino}
-\usepackage{mathpazo}
-\pagestyle{empty}
-\begin{document}
-__REPLACE_ME__ % this is where your LaTeX expression goes between $$
-\end{document}
-
-
-
-
diff --git a/content/icon.png b/content/icon.png
deleted file mode 100644
index 937ab3f..0000000
Binary files a/content/icon.png and /dev/null differ
diff --git a/content/insert.js b/content/insert.js
deleted file mode 100644
index c74e846..0000000
--- a/content/insert.js
+++ /dev/null
@@ -1,67 +0,0 @@
-function on_ok() {
- var latex = document.getElementById("tblatex-expr").value;
- var autodpi = document.getElementById("autodpi-checkbox").checked;
- var font_px = document.getElementById("fontpx").value;
- window.arguments[0](latex, autodpi, font_px);
-}
-
-window.addEventListener("load", function (event) {
- var template = window.arguments[1];
- var selection = window.arguments[2];
- populate(template, selection);
- var autodpi = prefs.getBoolPref("autodpi");
- document.getElementById("autodpi-checkbox").checked = autodpi;
- update_ui(autodpi);
- var font_px = prefs.getIntPref("font_px");
- document.getElementById("fontpx").value = font_px;
-}, false);
-
-function on_reset() {
- var template = prefs.getCharPref("template");
- populate(template, null);
-}
-
-function on_autodpi() {
- var autodpi = document.getElementById("autodpi-checkbox").checked;
- update_ui(autodpi);
-}
-
-var prefs = Components.classes["@mozilla.org/preferences-service;1"]
- .getService(Components.interfaces.nsIPrefService)
- .getBranch("tblatex.");
-
-function populate(template, selection) {
- var marker = "__REPLACE_ME__";
- var oldmarker = "__REPLACEME__";
- var start = template.indexOf(marker);
- if (start < 0) {
- start = template.indexOf(oldmarker);
- if (start > -1) {
- marker = oldmarker;
- }
- }
- if (start > -1)
- if (selection) {
- // Replace marker with selection
- template = template.substring(0, start) + selection + template.substring(start+marker.length)
- marker = selection;
- }
- else {
- // Insert $ on either side of the marker, so it's easier for the user
- template = template.slice(0, start) + "$" + template.slice(start, start+marker.length) + "$" + template.slice(start+marker.length);
- start += 1
- }
-
- var textarea = document.getElementById("tblatex-expr");
- textarea.value = template;
- textarea.focus();
- if (start > -1)
- // Select marker or selection
- textarea.setSelectionRange(start, start+marker.length);
-}
-
-function update_ui(autodpi) {
- document.getElementById("fontpx-label").disabled = autodpi;
- document.getElementById("fontpx").disabled = autodpi;
- document.getElementById("fontpx-unit").disabled = autodpi;
-}
diff --git a/content/insert.xul b/content/insert.xul
deleted file mode 100644
index 70ab061..0000000
--- a/content/insert.xul
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
diff --git a/content/main.js b/content/main.js
deleted file mode 100644
index 3bcb076..0000000
--- a/content/main.js
+++ /dev/null
@@ -1,779 +0,0 @@
-var tblatex = {
- on_latexit: null,
- on_middleclick: null,
- on_undo: null,
- on_undo_all: null,
- on_insert_complex: null,
- on_open_options: null
-};
-
-(function () {
- var isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
-
- if (document.location.href != "chrome://messenger/content/messengercompose/messengercompose.xul")
- return;
-
- var g_undo_func = null;
- var g_image_cache = {};
-
- function dumpCallStack(e) {
- let frame = e ? e.stack : Components.stack;
- while (frame) {
- dump("\n"+frame);
- frame = frame.caller;
- }
- }
-
- function push_undo_func(f) {
- var old_undo_func = g_undo_func;
- var g = function () {
- g_undo_func = old_undo_func;
- f();
- };
- g_undo_func = g;
- }
-
- var prefs = Components.classes["@mozilla.org/preferences-service;1"]
- .getService(Components.interfaces.nsIPrefService)
- .getBranch("tblatex.");
-
- function insertAfter(node1, node2) {
- var parentNode = node2.parentNode;
- if (node2.nextSibling)
- parentNode.insertBefore(node1, node2.nextSibling);
- else
- parentNode.appendChild(node1);
- }
-
- /* splits #text [ some text $\LaTeX$ some text ] into three separates text
- * nodes and returns the list of latex nodes */
- function split_text_nodes(node) {
- var latex_nodes = [];
- if (node.nodeType == node.TEXT_NODE) {
- var re = /\$\$[^\$]+\$\$|\$[^\$]+\$|\\\[.*?\\\]|\\\(.*?\\\)/g;
- var matches = node.nodeValue.match(re);
- if (matches) {
- for (var i = matches.length - 1; i >= 0; --i) (function (i) {
- var match = matches[i];
- var j = node.nodeValue.lastIndexOf(match);
- var k = j + match.length;
- insertAfter(node.ownerDocument.createTextNode(node.nodeValue.substr(k, (node.nodeValue.length-k))), node);
- var latex_node = node.ownerDocument.createTextNode(match);
- latex_nodes.push(latex_node);
- insertAfter(latex_node, node);
- node.nodeValue = node.nodeValue.substr(0, j);
- })(i);
- }
- } else if (node.childNodes && node.childNodes.length) {
- for (var i = node.childNodes.length - 1; i >= 0; --i) {
- if (i > 0 && node.childNodes[i-1].nodeType == node.childNodes[i-1].TEXT_NODE && node.childNodes[i].nodeValue) {
- node.childNodes[i-1].nodeValue += node.childNodes[i].nodeValue;
- node.childNodes[i].nodeValue = "";
- continue;
- }
- latex_nodes = latex_nodes.concat(split_text_nodes(node.childNodes[i]));
- }
- }
- return latex_nodes;
- }
-
- /* This *has* to be global. If image a.png is inserted, then modified, then
- * inserted again in the same mail, the OLD a.png is displayed because of some
- * cache which I haven't found a way to invalidate yet. */
- var g_suffix = 1;
-
- /* Returns [st, src, depth, log] where :
- * - st is 0 if everything went ok, 1 if some error was found but the image
- * was nonetheless generated, 2 if there was a fatal error
- * - src is the local path of the image if generated
- * - depth is the number of pixels from the bottom of the image to the baseline of the image
- * - log is the log messages generated during the run
- * */
- function run_latex(latex_expr, font_px, font_color) {
- var log = "";
- var st = 0;
- var temp_file;
- try {
- var deletetempfiles = !prefs.getBoolPref("keeptempfiles");
- var debug = prefs.getBoolPref("debug");
- if (debug)
- log += ("\n*** Generating LaTeX expression:\n"+latex_expr+"\n");
-
- if (g_image_cache[latex_expr+font_px+font_color]) {
- if (debug)
- log += "Found a cached image file "+g_image_cache[latex_expr+font_px+font_color].png+" (depth="+g_image_cache[latex_expr+font_px+font_color].depth+"), returning\n";
- return [0, g_image_cache[latex_expr+font_px+font_color].png, g_image_cache[latex_expr+font_px+font_color].depth, log+"Image was already generated\n"];
- }
-
- // Check if the LaTeX expression (that is, the whole file) contains the required packages.
- // At the moment, it checks for the minimum of
- // - \usepackage[active]{preview}
- // which must not be commented out.
- //
- // The 'preview' package is needed for the baseline alignment with the surrounding text
- // introduced in v0.7.x.
- //
- // If the package(s) cannot be found, an alert message window is shown, informing the user.
- var re = /^[^%]*\\usepackage\[(.*,\s*)?active(,.*)?\]{(.*,\s*)?preview(,.*)?}/m;
- var package_match = latex_expr.match(re);
- if (!package_match) {
- alert("Latex It! Error - Nothing added!\n\nThe package 'preview' cannot be found in the LaTeX file.\nThe inclusion of the LaTeX package 'preview' (with option 'active') is mandatory for the generated pictures to be aligned with the surrounding text!\n\nSolution:\n\tInsert a line with\n\t\t\\usepackage[active,displaymath,textmath]{preview}\n\tin the preamble of your LaTeX template or complex expression.");
- log += "!!! The package 'preview' cannot be found in the LaTeX file.\n";
- return [2, "", 0, log];
- }
-
- var init_file = function(path) {
- var f = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
- try {
- f.initWithPath(path);
- return f;
- } catch (e) {
- alert("Latex It! Error\n\nThis path is malformed:\n\t"+path+"\n\nSolution:\n\tSet the path properly in the add-on's options dialog (☰>Add-ons>Latex It!)");
- log += "!!! This path is malformed: "+path+".\n"+
- "Possible reasons include: you didn't setup the paths properly in the add-on's options.\n";
- return {
- exists: function () { return false; }
- };
- }
- }
- var init_process = function(path) {
- var process = Components.classes["@mozilla.org/process/util;1"].createInstance(Components.interfaces.nsIProcess);
- process.init(path);
- return process;
- }
- var sanitize_arg = function(arg) {
- // on Windows the nsIProcess function will add quotes around all arguments with spaces for us
- if (isWindows)
- return arg;
- else if (arg.indexOf(" ") < 0)
- return arg;
- else
- return "\""+arg+"\"";
- }
-
- var latex_bin = init_file(prefs.getCharPref("latex_path"));
- if (!latex_bin.exists()) {
- alert("Latex It! Error\n\nThe 'latex' executable cannot be found.\n\nSolution:\n\tSet the right path in the add-on's options dialog (☰>Add-ons>Latex It!)");
- log += "!!! Wrong path for 'latex' executable. Please set the right path in the options dialog first.\n";
- return [2, "", 0, log];
- }
- var dvipng_bin = init_file(prefs.getCharPref("dvipng_path"));
- if (!dvipng_bin.exists()) {
- alert("Latex It! Error\n\nThe 'dvipng' executable cannot be found.\n\nSolution:\n\tSet the right path in the add-on's options dialog (☰>Add-ons>Latex It!)");
- log += "!!! Wrong path for 'dvipng' executable. Please set the right path in the options dialog first.\n";
- return [2, "", 0, log];
- }
- // Since version 0.7.1 we support the alignment of the inserted pictures
- // to the text baseline, which works as follows (see also
- // https://github.com/protz/LatexIt/issues/36):
- // 1. Have the LaTeX package preview available.
- // 2. Insert \usepackage[active,textmath]{preview} into the preamble of
- // the LaTeX document.
- // 3. Run dvipng with the option --depth.
- // 4. Parse the output of the command for the depth value (a typical
- // output is:
- // This is dvipng 1.15 Copyright 2002-2015 Jan-Ake Larsson
- // [1 depth=4]
- // 5. Return the depth value (in the above case 4) from
- // 'main.js:run_latex()' in addition to the values already returned.
- // 6. In 'content/main.js' replace all
- // 'img.style = "vertical-align: middle"' with
- // 'img.style = "vertical-align: -px"' (where is the
- // value returned by dvipng and needs a - sign in front of it).
- // The problem lies in the step 4, because it looks like that it is not
- // possible to capture the output of an external command in Thunderbird
- // (https://stackoverflow.com/questions/10215643/how-to-execute-a-windows-command-from-firefox-addon#answer-10216452).
- // However it is possible to redirect the standard output into a temporary
- // file and parse that file: You need to call the command in an external
- // shell (or LatexIt! must call a special script doing the redirection,
- // which should also be avoided, because it requires installing this
- // script file).
- // Here we get the shell binary and the command line option to call an
- // external program.
- if (isWindows) {
- var env = Components.classes["@mozilla.org/process/environment;1"]
- .getService(Components.interfaces.nsIEnvironment);
- var shell_bin = init_file(env.get("COMSPEC"));
- var shell_option = "/C";
- } else {
- var shell_bin = init_file("/bin/sh");
- var shell_option = "-c";
- }
-
- var temp_dir = Components.classes["@mozilla.org/file/directory_service;1"].
- getService(Components.interfaces.nsIProperties).
- get("TmpD", Components.interfaces.nsIFile).path;
- temp_file = init_file(temp_dir);
- temp_file.append("tblatex-"+g_suffix+".png");
- while (temp_file.exists()) {
- g_suffix++;
- temp_file = init_file(temp_dir);
- temp_file.append("tblatex-"+g_suffix+".png");
- }
- var temp_file_noext = "tblatex-"+g_suffix;
- temp_file = init_file(temp_dir);
- temp_file.append("tblatex-"+g_suffix+".tex");
- if (temp_file.exists()) temp_file.remove(false);
-
- // file is nsIFile, data is a string
- var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
- createInstance(Components.interfaces.nsIFileOutputStream);
-
- // use 0x02 | 0x10 to open file for appending.
- foStream.init(temp_file, 0x02 | 0x08 | 0x20, 0666, 0);
- // write, create, truncate
- // In a c file operation, we have no need to set file mode with or operation,
- // directly using "r" or "w" usually.
-
- // if you are sure there will never ever be any non-ascii text in data you can
- // also call foStream.writeData directly
- var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
- createInstance(Components.interfaces.nsIConverterOutputStream);
- converter.init(foStream, "UTF-8", 0, 0);
- converter.writeString(latex_expr);
- converter.close(); // this closes foStream
-
-
- var latex_process = init_process(latex_bin);
- var latex_args = ["-output-directory="+temp_dir, "-interaction=batchmode", temp_file.path];
- // This adds quotes around all arguments that contains spaces but only on Unix (on Windows the nsIProcess function will add quotes around all arguments with spaces for us)
- latex_args = latex_args.map(sanitize_arg);
- latex_process.run(true, latex_args, latex_args.length);
- if (debug)
- log += "I ran "+sanitize_arg(latex_bin.path)+" "+latex_args.join(" ")+" error code "+latex_process.exitValue+"\n";
- if (latex_process.exitValue) {
- st = 1;
- log += "LaTeX process returned "+latex_process.exitValue+"\nProceeding anyway...\n";
- }
-
- ["log", "aux"].forEach(function (ext) {
- var file = init_file(temp_dir);
- file.append(temp_file_noext+"."+ext);
- if (deletetempfiles) file.remove(false);
- });
-
- var dvi_file = init_file(temp_dir);
- dvi_file.append(temp_file_noext+".dvi");
- if (!dvi_file.exists()) {
- // alert("Latex It! Error\n\nLaTeX did not output a .dvi file.\n\nSolution:\n\tWe left the .tex file there:\n\t\t"+temp_file.path+"\n\tTry to run 'latex' on it by yourself...");
- log += "!!! LaTeX did not output a .dvi file, something definitely went wrong. Aborting.\n";
- return [2, "", 0, log];
- }
-
- var png_file = init_file(temp_dir);
- png_file.append(temp_file_noext+".png");
- var depth_file = init_file(temp_dir);
- depth_file.append(temp_file_noext+"-depth.txt");
-
- // Output resolution to fit font size (see 'man dvipng', option -D) for LaTeX default font height 10 pt
- //
- // -D num
- // Set the output resolution, both horizontal and vertical, to num dpi
- // (dots per inch).
- //
- // One may want to adjust this to fit a certain text font size (e.g.,
- // on a web page), and for a text font height of font_px pixels (in
- // Mozilla) the correct formula is
- //
- // = * 72.27 / 10 [px * TeXpt/in / TeXpt]
- //
- // The last division by ten is due to the standard font height 10pt in
- // your document, if you use 12pt, divide by 12. Unfortunately, some
- // proprietary browsers have font height in pt (points), not pixels.
- // You have to rescale that to pixels, using the screen resolution
- // (default is usually 96 dpi) which means the formula is
- //
- // = * 96 / 72 [pt * px/in / (pt/in)]
- //
- // On some high-res screens, the value is instead 120 dpi. Good luck!
- //
- // Looks like Thunderbird is one of the "proprietary browsers", at least if I assumed that
- // the font size returned is in points (and not pixels) I get the right size with a screen
- // resolution of 96.
- //
- // -z 0-9
- // Set the PNG compression level to num. The default compression level
- // is 1, which selects maximum speed at the price of slightly larger
- //
- // PNGs. The include file png.h says "Currently, valid values range
- // from 0 - 9, corresponding directly to the zlib compression levels
- // 0 - 9 (0 - no compression, 9 - "maximal" compression). Note that tests
- // have shown that zlib compression levels 3-6 usually perform as well as
- // level 9 for PNG images, and do considerably fewer calculations. In the
- // future, these values may not correspond directly to the zlib compression
- // levels."
- //
- // As a compromise we use level 3.
- //
- // -bg color_spec
- // Choose background color for the images. This option will be ignored
- // if there is a background color \special in the DVI. The color spec
- // should be in TeX color \special syntax, e.g., 'rgb 1.0 0.0 0.0'.
- // You can also specify 'Transparent' or 'transparent' which will give
- // you a transparent background with the normal background as a
- // fallback color. A capitalized 'Transparent' will give a full-alpha
- // transparency, while an all-lowercase 'transparent' will give a
- // simple fully transparent background with non-transparent
- // antialiased pixels. The latter would be suitable for viewers who
- // cannot cope with a true alpha channel. GIF images do not support
- // full alpha transparency, so in case of GIF output, both variants
- // will use the latter behaviour.
- //
- // We simply assume, that all modern mail viewers can handle a true alpha channel,
- // hence we use "Transparent".
- if (prefs.getBoolPref("autodpi") && font_px) {
- var font_size = parseFloat(font_px);
- if (debug)
- log += "*** Surrounding text has font size of "+font_px+"\n";
- } else {
- var font_size = prefs.getIntPref("font_px");
- if (debug)
- log += "*** Using font size "+font_size+"px set in preferences\n";
- }
- var dpi = font_size * 72.27 / 10;
- if (debug)
- log += "*** Calculated resolution is "+dpi+" dpi\n";
-
- var shell_process = init_process(shell_bin);
- var dvipng_args = [dvipng_bin.path, "--depth", "-T", "tight", "-z", "3", "-bg", "Transparent", "-D", dpi.toString(), "-fg", font_color, "-o", png_file.path, dvi_file.path, ">", depth_file.path];
- // This adds quotes around all arguments that contains spaces but only on Unix (on Windows the nsIProcess function will add quotes around all arguments with spaces for us)
- dvipng_args = dvipng_args.map(sanitize_arg);
- if (isWindows) {
- // The additional echo commands are needed on Windows in order to remove the unwanded backslaches and quotes added by the nsIProcess function ( shell_process.run )
- var prefix_args = ["echo ", "\"", "&"];
- var suffix_args = ["&", "echo", "\""];
- var process_args = [shell_option].concat(prefix_args, dvipng_args, suffix_args);
- } else {
- var process_args = [shell_option, dvipng_args.join(" ")];
- }
- shell_process.run(true, process_args, process_args.length);
- if (deletetempfiles) dvi_file.remove(false);
- if (debug)
- log += "I ran "+shell_bin.path+" -c '"+dvipng_args.join(" ")+"'\n";
- if (shell_process.exitValue) {
- // alert("Latex It! Error\n\nWhen converting the .dvi to a .png bitmap, 'dvipng' failed (Error code: "+shell_process.exitValue+")\n\nSolution:\n\tWe left the .dvi file there:\n\t\t"+temp_file.path+"\n\tTry to run 'dvipng --depth' on it by yourself...");
- log += "!!! dvipng failed with error code "+shell_process.exitValue+". Aborting.\n";
- return [2, "", 0, log];
- }
-
- if (debug) {
- log += ("*** Status is "+st+"\n");
- log += ("*** Path is "+png_file.path+"\n");
- }
-
- // We must leave some time for the window manager to actually get rid of the
- // old terminal windows that pop up on Windows when launching latex.
- if (isWindows) {
- setTimeout(function () {
- window.focus();
- }, 500);
- }
-
- // Read the depth (distance between base of image and baseline) from the depth file
- if (!depth_file.exists()) {
- log += "dvipng did not output a depth file. Continuing without alignment.\n";
- g_image_cache[latex_expr+font_px+font_color] = {png: png_file.path, depth: 0};
- return [st, png_file.path, 0, log];
- }
-
- // https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Code_snippets/File_I_O#Line_by_line
- // Open an input stream from file
- var istream = Components.classes["@mozilla.org/network/file-input-stream;1"].
- createInstance(Components.interfaces.nsIFileInputStream);
- istream.init(depth_file, 0x01, 0444, 0);
- istream.QueryInterface(Components.interfaces.nsILineInputStream);
-
- // Read line by line and look for the depth information, which is contained in a line such as
- // [1 depth=4]
- var re = /^\[[0-9] +depth=([0-9]+)\] *$/;
- var line = {}, hasmore;
- var depth = 0;
- do {
- hasmore = istream.readLine(line);
- var linematch = line.value.match(re);
- if (linematch) {
- // Matching line found, get depth information and exit loop
- depth = linematch[1];
- if (debug)
- log += ("*** Depth is "+depth+"\n");
- break;
- }
- } while(hasmore);
-
- // Close input stream
- istream.close();
-
- if (deletetempfiles) depth_file.remove(false);
-
- // Only delete the temporary file at this point, so that it's left on disk
- // in case of error.
- if (deletetempfiles) temp_file.remove(false);
-
- g_image_cache[latex_expr+font_px+font_color] = {png: png_file.path, depth: depth};
- return [st, png_file.path, depth, log];
- } catch (e) {
- // alert("Latex It! Error\n\nSevere error. Missing package?\n\nSolution:\n\tWe left the .tex file there:\n\t\t"+temp_file.path+"\n\tTry to run 'latex' and 'dvipng --depth' on it by yourself...");
- dump(e+"\n");
- dump(e.stack+"\n");
- log += "!!! Severe error. Missing package?\n";
- log += "We left the .tex file there: "+temp_file.path+", try to run 'latex' and 'dvipng --depth' on it by yourself...\n";
- return [2, "", 0, log];
- }
- }
-
- function open_log() {
- var want_log = prefs.getBoolPref("log");
- if (!want_log)
- return (function () {});
-
- var editor = document.getElementById("content-frame");
- var edocument = editor.contentDocument;
- var body = edocument.getElementsByTagName("body")[0];
- var div = edocument.createElement("div");
- if (body.firstChild)
- body.insertBefore(div, body.firstChild);
- else
- body.appendChild(div);
- div.setAttribute("id", "tblatex-log");
- div.setAttribute("style", "border: 1px solid #333; position: relative; width: 500px;"+
- "-moz-border-radius: 5px; -moz-box-shadow: 2px 2px 6px #888; margin: 1em; padding: .5em;");
- div.innerHTML = "X"+
- ""+
- "LatexIt! run report... ";
- var a = div.getElementsByTagName("a")[0];
- a.addEventListener('click', {
- handleEvent: function (event) {
- a.parentNode.parentNode.removeChild(a.parentNode);
- return false
- }
- }, false);
- var p = edocument.createElement("pre");
- p.setAttribute("style", "max-height: 500px; overflow: auto;");
- div.appendChild(p);
- var f = function (text) { var n = edocument.createTextNode(text); p.appendChild(n); };
- return f;
- }
-
- function close_log() {
- var editor = document.getElementById("content-frame");
- var edocument = editor.contentDocument;
- var div = edocument.getElementById("tblatex-log");
- if (div)
- div.parentNode.removeChild(div);
- }
-
- function replace_marker(string, replacement) {
- var marker = "__REPLACE_ME__";
- var oldmarker = "__REPLACEME__";
- var len = marker.length;
- var log = "";
- var i = string.indexOf(marker);
- if (i < 0) {
- // Look for old marker
- i = string.indexOf(oldmarker);
- if (i < 0) {
- log += "\n!!! Could not find the placeholder '" + marker + "' in your template.\n";
- log += "This would be the place, where your LaTeX expression is inserted.\n";
- log += "Please edit your template and add this placeholder.\n";
- return [, log];
- } else {
- len = oldmarker.length;
- }
- }
- var p1 = string.substring(0, i);
- var p2 = string.substring(i+len);
- return [p1 + replacement + p2, log];
- }
-
- /* replaces each latex text node with the corresponding generated image */
- function replace_latex_nodes(nodes, silent) {
- var debug = prefs.getBoolPref("debug");
- var template = prefs.getCharPref("template");
- var write_log_func = null;
- var write_log = function(str) { if (!write_log_func) write_log_func = open_log(); return write_log_func(str); };
- var editor = GetCurrentEditor();
- if (!nodes.length && !silent)
- write_log("No unconverted LaTeX $$ expression was found\n");
- for (var i = 0; i < nodes.length; ++i) (function (i) { /* Need a real scope here and there is no let-binding available in Thunderbird 2 */
- var elt = nodes[i];
- if (!silent)
- write_log("*** Found expression "+elt.nodeValue+"\n");
- var [latex_expr, log] = replace_marker(template, elt.nodeValue);
- if (log)
- write_log(log);
- // Font size in pixels
- var font_px = window.getComputedStyle(elt.parentElement, null).getPropertyValue('font-size');
- // Font color in "rgb(x,y,z)" => "RGB x y z"
- var font_color = window.getComputedStyle(elt.parentElement, null).getPropertyValue('color').replace(/([\(,\)])/g, " ").replace("rgb", "RGB");
- var [st, url, depth, log] = run_latex(latex_expr, font_px, font_color);
- if (st || !silent)
- write_log(log);
- if (st == 0 || st == 1) {
- if (debug)
- write_log("--> Replacing node... ");
- var img = editor.createElementWithDefaults("img");
- var reader = new FileReader();
- var xhr = new XMLHttpRequest();
-
- xhr.addEventListener("load",function() {
- reader.readAsDataURL(xhr.response);
- },false);
-
- reader.addEventListener("load", function() {
- elt.parentNode.insertBefore(img, elt);
- elt.parentNode.removeChild(elt);
-
- img.alt = elt.nodeValue;
- img.style = "vertical-align: -" + depth + "px";
- img.src = reader.result;
-
- push_undo_func(function () {
- img.parentNode.insertBefore(elt, img);
- img.parentNode.removeChild(img);
- });
- if (debug)
- write_log("done\n");
- }, false);
-
- xhr.open('GET',"file://"+url);
- xhr.responseType = 'blob';
- xhr.overrideMimeType("image/png");
- xhr.send();
- } else {
- if (debug)
- write_log("--> Failed, not inserting\n");
- }
- })(i);
- }
-
- tblatex.on_latexit = function (event, silent) {
- /* safety checks */
- if (event.button == 2) return;
- var editor_elt = document.getElementById("content-frame");
- if (editor_elt.editortype != "htmlmail") {
- alert("Latex It! Error\n\nCannot Latexify plain text emails.\n\nSolution:\n\tStart again by opening the message composer window in HTML mode, this can be achieved by holding the 'Shift' key while pressing the button.");
- return;
- }
-
- var editor = GetCurrentEditor();
- editor.beginTransaction();
- try {
- close_log();
- var body = editor_elt.contentDocument.getElementsByTagName("body")[0];
- var latex_nodes = split_text_nodes(body);
- replace_latex_nodes(latex_nodes, silent);
- } catch (e /*if false*/) { /*XXX do not catch errors to get full backtraces in dev cycles */
- Components.utils.reportError("TBLatex error: "+e);
- dump(e+"\n");
- dumpCallStack(e);
- }
- editor.endTransaction();
- };
-
- tblatex.on_middleclick = function(event) {
- // Return on all but the middle button
- if (event.button != 1) return;
-
- if (event.shiftKey) {
- // Undo all
- undo_all();
- } else {
- // Undo
- undo();
- }
- event.stopPropagation();
- };
-
- tblatex.on_undo = function (event) {
- undo();
- event.stopPropagation();
- };
-
- function undo() {
- var editor = GetCurrentEditor();
- editor.beginTransaction();
- try {
- if (g_undo_func)
- g_undo_func();
- } catch (e) {
- Components.utils.reportError("TBLatex Error (while undoing) "+e);
- dumpCallStack(e);
- }
- editor.endTransaction();
- }
-
- tblatex.on_undo_all = function (event) {
- undo_all();
- event.stopPropagation();
- };
-
- function undo_all() {
- var editor = GetCurrentEditor();
- editor.beginTransaction();
- try {
- while (g_undo_func)
- g_undo_func();
- } catch (e) {
- Components.utils.reportError("TBLatex Error (while undoing) "+e);
- dumpCallStack(e);
- }
- editor.endTransaction();
- };
-
- var g_complex_input = null;
-
- tblatex.on_insert_complex = function (event) {
- var editor = GetCurrentEditor();
- var f = function (latex_expr, autodpi, font_size) {
- var debug = prefs.getBoolPref("debug");
- g_complex_input = latex_expr;
- editor.beginTransaction();
- try {
- close_log();
- var write_log = open_log();
- if (autodpi) {
- // Font size at cursor position
- var elt = editor.selection.anchorNode.parentElement;
- var font_px = window.getComputedStyle(elt).getPropertyValue('font-size');
- } else {
- var font_px = font_size+"px";
- }
- // Font color in "rgb(x,y,z)" => "RGB x y z"
- var font_color = window.getComputedStyle(elt).getPropertyValue('color').replace(/([\(,\)])/g, " ").replace("rgb", "RGB");
- var [st, url, depth, log] = run_latex(latex_expr, font_px, font_color);
- log = log || "Everything went OK.\n";
- write_log(log);
- if (st == 0 || st == 1) {
- if (debug)
- write_log("--> Inserting at cursor position... ");
- var img = editor.createElementWithDefaults("img");
- var reader = new FileReader();
- var xhr = new XMLHttpRequest();
-
- xhr.addEventListener("load",function() {
- reader.readAsDataURL(xhr.response);
- },false);
-
- reader.addEventListener("load", function() {
- editor.insertElementAtSelection(img, true);
-
- img.alt = latex_expr;
- img.title = latex_expr;
- img.style = "vertical-align: -" + depth + "px";
- img.src = reader.result;
-
- push_undo_func(function () {
- img.parentNode.removeChild(img);
- });
- if (debug)
- write_log("done\n");
- }, false);
-
- xhr.open('GET',"file://"+url);
- xhr.responseType = 'blob';
- xhr.overrideMimeType("image/png");
- xhr.send();
- } else {
- if (debug)
- write_log("--> Failed, not inserting\n");
- }
- } catch (e) {
- Components.utils.reportError("TBLatex Error (while inserting) "+e);
- dumpCallStack(e);
- }
- editor.endTransaction();
- };
- var template = g_complex_input || prefs.getCharPref("template");
- var selection = editor.selection.toString();
- window.openDialog("chrome://tblatex/content/insert.xul", "", "chrome, resizable=yes", f, template, selection);
- event.stopPropagation();
- };
-
- tblatex.on_open_options = function (event) {
- window.openDialog("chrome://tblatex/content/options.xul", "", "");
- event.stopPropagation();
- };
-
- function check_log_report () {
- // code from close_log();
- var editor = document.getElementById("content-frame");
- var edocument = editor.contentDocument;
- var div = edocument.getElementById("tblatex-log");
- if (div) {
- var retVals = {action: -1};
- window.openDialog("chrome://tblatex/content/sendalert.xul", "", "chrome,modal,dialog,centerscreen", retVals);
- switch (retVals.action) {
- case 0:
- div.parentNode.removeChild(div);
- return true;
- case 1:
- return true;
- default:
- return false;
- }
- } else {
- return true;
- }
- }
-
- // Override original send functions (this follows the approach from the "Check and Send" add-on
- var tblatex_SendMessage_orig = SendMessage;
- SendMessage = function() {
- if (check_log_report())
- tblatex_SendMessage_orig.apply(this, arguments);
- }
-
- // Ctrl-Enter
- var tblatex_SendMessageWithCheck_orig = SendMessageWithCheck;
- SendMessageWithCheck = function() {
- if (check_log_report())
- tblatex_SendMessageWithCheck_orig.apply(this, arguments);
- }
-
- var tblatex_SendMessageLater_orig = SendMessageLater;
- SendMessageLater = function() {
- if (check_log_report())
- tblatex_SendMessageLater_orig.apply(this, arguments);
- }
-
- /* Is this even remotey useful ? */
- /* Yes, because we can disable the toolbar button and menu items for plain text messages! */
- window.addEventListener("load",
- function () {
- var tb = document.getElementById("composeToolbar2");
- tb.setAttribute("defaultset", tb.getAttribute("defaultset")+",tblatex-button-1");
-
- // Disable the button and menu for non-html composer windows
- var editor_elt = document.getElementById("content-frame");
- if (editor_elt.editortype != "htmlmail") {
- var btn = document.getElementById("tblatex-button-1");
- if (btn) {
- btn.tooltipText = "Start a message in HTML format (by holding the 'Shift' key) to be able to turn every $...$ into a LaTeX image"
- btn.disabled = true;
- }
- for (var id of ["tblatex-context", "tblatex-context-menu"]) {
- var menu = document.getElementById(id);
- if (menu)
- menu.disabled = true;
- }
- }
- }, false);
-
- window.addEventListener("unload",
- // Remove all cached images on closing the composer window
- function() {
- if (!prefs.getBoolPref("keeptempfiles")) {
- for (var key in g_image_cache) {
- var f = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
- try {
- f.initWithPath(g_image_cache[key]);
- f.remove(false);
- delete g_image_cache[key];
- } catch (e) { }
- }
- }
- }, false);
-})()
diff --git a/content/options.js b/content/options.js
deleted file mode 100644
index 974c45e..0000000
--- a/content/options.js
+++ /dev/null
@@ -1,49 +0,0 @@
-function pick_file(pref, title) {
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
- fp.init(window, "Select the "+title+" binary", nsIFilePicker.modeOpen);
-
- fp.open(rv => {
- if( rv != nsIFilePicker.returnOK) {
- return;
- }
- pref.value = fp.file.path;
- });
-}
-
-function add_links(aDoc) {
- if (!window.Application) //TB 2.x will open this properly in an external browser
- return;
-
- var links = aDoc.getElementsByClassName("external");
- for (var i = 0; i < links.length; ++i) (function (i) {
- dump("link "+i+"\n");
- var uri = links[i].getAttribute("href");
- links[i].addEventListener("click",
- function link_listener (event) {
- if (!(uri instanceof Components.interfaces.nsIURI))
- uri = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService)
- .newURI(uri, null, null);
-
- Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
- .getService(Components.interfaces.nsIExternalProtocolService)
- .loadURI(uri);
-
- event.preventDefault();
- }, true);
- })(i);
-}
-
-function open_autodetect() {
- window.openDialog('chrome://tblatex/content/firstrun.html', '',
- 'all,chrome,dialog=no,status,toolbar,width=640,height=480', add_links);
-}
-
-window.addEventListener("load", function (event) {
- on_log();
-}, false);
-
-function on_log() {
- document.getElementById("debug_checkbox").disabled = !document.getElementById("log_checkbox").checked;
-}
diff --git a/content/options.xul b/content/options.xul
deleted file mode 100644
index 748fafa..0000000
--- a/content/options.xul
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
diff --git a/content/osx-path-hacks.js b/content/osx-path-hacks.js
deleted file mode 100644
index 7cdca04..0000000
--- a/content/osx-path-hacks.js
+++ /dev/null
@@ -1,22 +0,0 @@
-(function () {
- var isOSX = ("nsILocalFileMac" in Components.interfaces);
- var Cc = Components.classes;
- var Ci = Components.interfaces;
-
- if (isOSX) {
- var env = Cc["@mozilla.org/process/environment;1"].createInstance(Ci.nsIEnvironment);
- var paths = env.get("PATH").split(":");
- var suggestions = [
- "/usr/local/bin",
- "/usr/texbin",
- "/usr/X11/bin",
- "/usr/local/texlive/2016/bin/x86_64-darwin/"
- ];
- for (var i = 0; i < suggestions.length; ++i) {
- if (paths.indexOf(suggestions[i]) < 0)
- paths.push(suggestions[i]);
- }
- env.set("PATH", paths.join(":"));
- dump("New path: "+env.get("PATH")+"\n");
- }
-})();
diff --git a/content/overlay.xul b/content/overlay.xul
deleted file mode 100644
index 93a139e..0000000
--- a/content/overlay.xul
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/content/overlay_firstrun.xul b/content/overlay_firstrun.xul
deleted file mode 100644
index ce9245c..0000000
--- a/content/overlay_firstrun.xul
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/content/preferences.js b/content/preferences.js
deleted file mode 100644
index 86fddc3..0000000
--- a/content/preferences.js
+++ /dev/null
@@ -1,10 +0,0 @@
-Preferences.addAll([
- { id: "tblatex.latex_path", type: "string" },
- { id: "tblatex.dvipng_path", type: "string" },
- { id: "tblatex.autodpi", type: "bool" },
- { id: "tblatex.font_px", type: "int" },
- { id: "tblatex.log", type: "bool" },
- { id: "tblatex.debug", type: "bool" },
- { id: "tblatex.keeptempfiles", type: "bool" },
- { id: "tblatex.template", type: "string" },
-]);
diff --git a/content/sendalert.js b/content/sendalert.js
deleted file mode 100644
index 052ae8b..0000000
--- a/content/sendalert.js
+++ /dev/null
@@ -1,20 +0,0 @@
-function on_send() {
-console.log("LATEX OK *********");
- var retVals = window.arguments[0];
- retVals.action = 1;
- close();
-}
-
-function on_cancel() {
-console.log("LATEX Cancel *********");
- var retVals = window.arguments[0]
- retVals.action = -1;
- close();
-}
-
-function on_removeandsend(event) {
-console.log("LATEX Remove and send *********");
- var retVals = window.arguments[0]
- retVals.action = 0;
- close();
-}
diff --git a/content/sendalert.xul b/content/sendalert.xul
deleted file mode 100644
index d63e2fb..0000000
--- a/content/sendalert.xul
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
diff --git a/defaults/preferences/defaults.js b/defaults/preferences/defaults.js
deleted file mode 100644
index 5cbf3fa..0000000
--- a/defaults/preferences/defaults.js
+++ /dev/null
@@ -1,10 +0,0 @@
-pref("tblatex.latex_path", "");
-pref("tblatex.dvipng_path", "");
-pref("tblatex.autodpi", true);
-pref("tblatex.font_px", 16);
-pref("tblatex.log", true);
-pref("tblatex.debug", false);
-pref("tblatex.keeptempfiles", false);
-pref("tblatex.template", "\\documentclass{article}\n\\usepackage[utf8]{inputenc}\n\\usepackage[active,displaymath,textmath]{preview} % DO NOT DELETE - this is required for baseline alignment\n\\pagestyle{empty}\n\\begin{document}\n__REPLACE_ME__ % this is where your LaTeX expression goes between $$\n\\end{document}\n");
-pref("tblatex.firstrun", 0);
-
diff --git a/helper/README.md b/helper/README.md
new file mode 100644
index 0000000..fe3a040
--- /dev/null
+++ b/helper/README.md
@@ -0,0 +1,50 @@
+# LaTeX It! Local Helper
+
+Use this helper when Thunderbird is sandboxed (for example Snap), so the
+extension can render LaTeX by calling a local HTTP service outside the sandbox.
+
+## Preferred (Linux): systemd user service
+
+Install and start:
+
+```sh
+bash helper/install-systemd-user.sh
+```
+
+Service management:
+
+```sh
+systemctl --user status tblatex-helper.service
+systemctl --user restart tblatex-helper.service
+journalctl --user -u tblatex-helper.service -f
+```
+
+Uninstall service:
+
+```sh
+bash helper/uninstall-systemd-user.sh
+```
+
+Uninstall service and helper config:
+
+```sh
+bash helper/uninstall-systemd-user.sh --purge-config
+```
+
+## Fallback: run manually
+
+```sh
+python3 helper/tblatex_helper.py
+```
+
+Default listen address:
+- `http://127.0.0.1:3737`
+
+Optional env vars:
+- `TBLATEX_HELPER_HOST` (default `127.0.0.1`)
+- `TBLATEX_HELPER_PORT` (default `3737`)
+
+## Endpoints
+
+- `GET /health`
+- `POST /render`
diff --git a/helper/install-systemd-user.sh b/helper/install-systemd-user.sh
new file mode 100755
index 0000000..f4afa06
--- /dev/null
+++ b/helper/install-systemd-user.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SERVICE_NAME="tblatex-helper.service"
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+SOURCE_SCRIPT="${SCRIPT_DIR}/tblatex_helper.py"
+
+XDG_DATA_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}"
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}"
+INSTALL_DIR="${XDG_DATA_HOME}/tblatex-helper"
+CONFIG_DIR="${XDG_CONFIG_HOME}/tblatex-helper"
+SYSTEMD_USER_DIR="${XDG_CONFIG_HOME}/systemd/user"
+TARGET_SCRIPT="${INSTALL_DIR}/tblatex_helper.py"
+ENV_FILE="${CONFIG_DIR}/env"
+UNIT_FILE="${SYSTEMD_USER_DIR}/${SERVICE_NAME}"
+
+if [[ ! -f "${SOURCE_SCRIPT}" ]]; then
+ echo "Missing helper source script: ${SOURCE_SCRIPT}" >&2
+ exit 1
+fi
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "python3 is required but not installed." >&2
+ exit 1
+fi
+
+if ! command -v systemctl >/dev/null 2>&1; then
+ echo "systemctl not found. Cannot install systemd user service." >&2
+ exit 1
+fi
+
+mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "${SYSTEMD_USER_DIR}"
+cp "${SOURCE_SCRIPT}" "${TARGET_SCRIPT}"
+chmod 0755 "${TARGET_SCRIPT}"
+
+if [[ ! -f "${ENV_FILE}" ]]; then
+ cat >"${ENV_FILE}" <<'EOF'
+TBLATEX_HELPER_HOST=127.0.0.1
+TBLATEX_HELPER_PORT=3737
+EOF
+ chmod 0644 "${ENV_FILE}"
+fi
+
+cat >"${UNIT_FILE}" <&2
+ exit 1
+fi
+
+if ! systemctl --user enable --now "${SERVICE_NAME}"; then
+ echo "Failed to enable/start ${SERVICE_NAME}. Try: systemctl --user status ${SERVICE_NAME}" >&2
+ exit 1
+fi
+
+echo "Installed and started ${SERVICE_NAME}."
+echo "Helper config: ${ENV_FILE}"
+echo "Service file: ${UNIT_FILE}"
+echo "Status: systemctl --user status ${SERVICE_NAME}"
diff --git a/helper/tblatex_helper.py b/helper/tblatex_helper.py
new file mode 100644
index 0000000..4c7c685
--- /dev/null
+++ b/helper/tblatex_helper.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+"""
+Local LaTeX rendering helper for sandboxed Thunderbird installations.
+
+Starts a localhost HTTP service with:
+ GET /health
+ POST /render
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from typing import Any
+
+PREVIEW_PACKAGE_RE = re.compile(
+ r"^[^%]*\\usepackage\[(.*,\s*)?active(,.*)?\]{(.*,\s*)?preview(,.*)?}",
+ re.MULTILINE,
+)
+FONT_NUMBER_RE = re.compile(r"[-+]?\d*\.?\d+")
+DEFAULT_RENDER_SCALE = 4.0
+
+
+def normalize_path(value: Any) -> str:
+ if not isinstance(value, str):
+ return ""
+ trimmed = value.strip()
+ if len(trimmed) >= 2 and trimmed[0] == trimmed[-1] and trimmed[0] in ("'", '"'):
+ trimmed = trimmed[1:-1].strip()
+ return trimmed
+
+
+def select_executable(requested_path: Any, executable_name: str) -> tuple[str, str]:
+ requested = normalize_path(requested_path)
+ if requested and os.path.exists(requested) and os.access(requested, os.X_OK):
+ return requested, requested
+ auto = shutil.which(executable_name) or ""
+ return auto, requested
+
+
+def parse_font_px(value: Any, fallback: int) -> float:
+ if isinstance(value, str):
+ match = FONT_NUMBER_RE.search(value)
+ if match:
+ try:
+ parsed = float(match.group(0))
+ if parsed > 0:
+ return parsed
+ except ValueError:
+ pass
+ try:
+ parsed = float(value)
+ if parsed > 0:
+ return parsed
+ except (TypeError, ValueError):
+ pass
+ return float(fallback)
+
+
+def parse_render_scale(value: Any, fallback: float = DEFAULT_RENDER_SCALE) -> float:
+ try:
+ parsed = int(value)
+ except (TypeError, ValueError):
+ parsed = int(fallback)
+ return float(min(8, max(1, parsed)))
+
+
+def run_command(args: list[str]) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ check=False,
+ )
+
+
+def render(payload: dict[str, Any]) -> dict[str, Any]:
+ latex_expression = payload.get("latexExpression", "")
+ if not isinstance(latex_expression, str):
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": "!!! Invalid latexExpression payload.\n",
+ }
+
+ if not PREVIEW_PACKAGE_RE.search(latex_expression):
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": "!!! The package 'preview' (active mode) cannot be found in the LaTeX template.\n",
+ }
+
+ latex_path, requested_latex_path = select_executable(payload.get("latexPath"), "latex")
+ if not latex_path:
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": (
+ "!!! Wrong path for 'latex' executable: "
+ f"\"{requested_latex_path or '(auto)'}\".\n"
+ ),
+ }
+
+ dvipng_path, requested_dvipng_path = select_executable(payload.get("dvipngPath"), "dvipng")
+ if not dvipng_path:
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": (
+ "!!! Wrong path for 'dvipng' executable: "
+ f"\"{requested_dvipng_path or '(auto)'}\".\n"
+ ),
+ }
+
+ keep_temp_files = bool(payload.get("keepTempFiles", False))
+ debug = bool(payload.get("debug", False))
+ autodpi = bool(payload.get("autodpi", True))
+ default_font_px_raw = payload.get("defaultFontPx", 16)
+ try:
+ default_font_px = int(default_font_px_raw)
+ if default_font_px <= 0:
+ default_font_px = 16
+ except (TypeError, ValueError):
+ default_font_px = 16
+
+ font_px_value = (
+ parse_font_px(payload.get("fontPx", ""), default_font_px)
+ if autodpi
+ else float(default_font_px)
+ )
+ render_scale = parse_render_scale(payload.get("renderScale", DEFAULT_RENDER_SCALE))
+ base_dpi = font_px_value * 72.27 / 10.0
+ dpi = base_dpi * render_scale
+ font_color = str(payload.get("fontColor", "")).strip() or "RGB 0 0 0"
+
+ status = 0
+ log_lines: list[str] = []
+
+ temp_dir = tempfile.mkdtemp(prefix="tblatex-helper-")
+ tex_file = os.path.join(temp_dir, "tblatex.tex")
+ dvi_file = os.path.join(temp_dir, "tblatex.dvi")
+ png_file = os.path.join(temp_dir, "tblatex.png")
+
+ try:
+ with open(tex_file, "w", encoding="utf-8") as handle:
+ handle.write(latex_expression)
+
+ latex_args = [
+ latex_path,
+ f"-output-directory={temp_dir}",
+ "-interaction=batchmode",
+ tex_file,
+ ]
+ latex_result = run_command(latex_args)
+ if latex_result.returncode != 0:
+ status = 1
+ log_lines.append(
+ f"LaTeX process returned {latex_result.returncode}. Proceeding anyway..."
+ )
+
+ if not os.path.exists(dvi_file):
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": (
+ "\n".join(log_lines)
+ + "\n!!! LaTeX did not output a .dvi file, something went wrong.\n"
+ ),
+ }
+
+ dvipng_args = [
+ dvipng_path,
+ "--depth",
+ "-T",
+ "tight",
+ "-z",
+ "3",
+ "-bg",
+ "Transparent",
+ "-D",
+ str(dpi),
+ "-fg",
+ font_color,
+ "-o",
+ png_file,
+ dvi_file,
+ ]
+ dvipng_result = run_command(dvipng_args)
+ if dvipng_result.returncode != 0 or not os.path.exists(png_file):
+ return {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": (
+ "\n".join(log_lines)
+ + f"\n!!! dvipng failed with code {dvipng_result.returncode}. Rendering aborted.\n"
+ ),
+ }
+
+ with open(png_file, "rb") as handle:
+ encoded = base64.b64encode(handle.read()).decode("ascii")
+
+ if keep_temp_files:
+ log_lines.append(f"Temporary files kept in {temp_dir}")
+
+ if debug:
+ log_lines.append(f"*** helper latex path: {latex_path}")
+ log_lines.append(f"*** helper dvipng path: {dvipng_path}")
+ log_lines.append(
+ f"*** helper dpi: {dpi} (base={base_dpi}, scale={render_scale}x)"
+ )
+ log_lines.append(f"*** helper font color: {font_color}")
+
+ return {
+ "status": status,
+ "depth": 0,
+ "dataUrl": f"data:image/png;base64,{encoded}",
+ "renderScale": render_scale,
+ "log": ("\n".join(log_lines) + "\n") if log_lines else "",
+ }
+ finally:
+ if not keep_temp_files:
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+
+class RequestHandler(BaseHTTPRequestHandler):
+ server_version = "tblatex-helper/0.1"
+
+ def _send_json(self, status_code: int, payload: dict[str, Any]) -> None:
+ body = json.dumps(payload).encode("utf-8")
+ self.send_response(status_code)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_OPTIONS(self) -> None: # noqa: N802
+ self.send_response(204)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+
+ def do_GET(self) -> None: # noqa: N802
+ route = self.path.split("?", 1)[0]
+ if route != "/health":
+ self._send_json(404, {"ok": False, "error": "not found"})
+ return
+
+ self._send_json(
+ 200,
+ {
+ "ok": True,
+ "service": "tblatex-helper",
+ "version": "0.1",
+ "latexAvailable": bool(shutil.which("latex")),
+ "dvipngAvailable": bool(shutil.which("dvipng")),
+ },
+ )
+
+ def do_POST(self) -> None: # noqa: N802
+ route = self.path.split("?", 1)[0]
+ if route != "/render":
+ self._send_json(404, {"ok": False, "error": "not found"})
+ return
+
+ try:
+ content_length = int(self.headers.get("Content-Length", "0"))
+ except ValueError:
+ self._send_json(400, {"ok": False, "error": "invalid content length"})
+ return
+
+ raw_body = self.rfile.read(content_length)
+ try:
+ payload = json.loads(raw_body.decode("utf-8"))
+ except (UnicodeDecodeError, json.JSONDecodeError):
+ self._send_json(400, {"ok": False, "error": "invalid json payload"})
+ return
+
+ try:
+ result = render(payload if isinstance(payload, dict) else {})
+ self._send_json(200, result)
+ except Exception as error: # pragma: no cover
+ self._send_json(
+ 500,
+ {
+ "status": 2,
+ "depth": 0,
+ "dataUrl": "",
+ "log": f"!!! Helper internal error: {error}\n",
+ },
+ )
+
+ def log_message(self, fmt: str, *args: Any) -> None:
+ print(f"[tblatex-helper] {self.address_string()} - {fmt % args}")
+
+
+def main() -> None:
+ host = os.environ.get("TBLATEX_HELPER_HOST", "127.0.0.1")
+ port_raw = os.environ.get("TBLATEX_HELPER_PORT", "3737")
+ try:
+ port = int(port_raw)
+ except ValueError:
+ port = 3737
+
+ server = ThreadingHTTPServer((host, port), RequestHandler)
+ print(f"tblatex-helper listening on http://{host}:{port}")
+ print("Press Ctrl+C to stop.")
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ print("\nStopping tblatex-helper...")
+ finally:
+ server.server_close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/helper/uninstall-systemd-user.sh b/helper/uninstall-systemd-user.sh
new file mode 100755
index 0000000..03397c6
--- /dev/null
+++ b/helper/uninstall-systemd-user.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SERVICE_NAME="tblatex-helper.service"
+
+XDG_DATA_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}"
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}"
+INSTALL_DIR="${XDG_DATA_HOME}/tblatex-helper"
+CONFIG_DIR="${XDG_CONFIG_HOME}/tblatex-helper"
+SYSTEMD_USER_DIR="${XDG_CONFIG_HOME}/systemd/user"
+TARGET_SCRIPT="${INSTALL_DIR}/tblatex_helper.py"
+ENV_FILE="${CONFIG_DIR}/env"
+UNIT_FILE="${SYSTEMD_USER_DIR}/${SERVICE_NAME}"
+
+if command -v systemctl >/dev/null 2>&1; then
+ systemctl --user disable --now "${SERVICE_NAME}" >/dev/null 2>&1 || true
+fi
+
+if [[ -f "${UNIT_FILE}" ]]; then
+ rm -f "${UNIT_FILE}"
+fi
+
+if command -v systemctl >/dev/null 2>&1; then
+ systemctl --user daemon-reload >/dev/null 2>&1 || true
+ systemctl --user reset-failed >/dev/null 2>&1 || true
+fi
+
+rm -f "${TARGET_SCRIPT}"
+rmdir "${INSTALL_DIR}" 2>/dev/null || true
+
+if [[ "${1:-}" == "--purge-config" ]]; then
+ rm -f "${ENV_FILE}"
+ rmdir "${CONFIG_DIR}" 2>/dev/null || true
+fi
+
+echo "Uninstalled ${SERVICE_NAME}."
+if [[ "${1:-}" != "--purge-config" ]]; then
+ echo "Config kept at ${ENV_FILE} (remove with --purge-config)."
+fi
diff --git a/manifest.json b/manifest.json
index 0cdd4b9..a12e41e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,22 +1,66 @@
{
"manifest_version": 2,
+ "name": "LaTeX It!",
+ "description": "Automatically change $\\LaTeX$ into images in your HTML mails.",
+ "version": "0.8.16",
+ "author": "Jonathan Protzenko",
+ "homepage_url": "https://github.com/protz/LatexIt/wiki",
"applications": {
"gecko": {
- "id": "tblatex@xulforum.org",
- "strict_min_version": "68.0a1",
- "strict_max_version": "73.*"
+ "id": "tblatex@andrewboldi.dev",
+ "strict_min_version": "140.0",
+ "strict_max_version": "140.*"
}
},
- "author": "Jonathan Protzenko",
- "name": "LaTeX It!",
- "description": "Automatically change $\\LaTeX$ into images in your HTML mails.",
- "version": "0.7.4",
- "homepage_url": "https://github.com/protz/LatexIt/wiki",
- "legacy": {
- "type": "xul",
- "options": {
- "page": "chrome://tblatex/content/options.xul",
- "open_in_tab": false
+ "permissions": [
+ "compose",
+ "http://127.0.0.1/*",
+ "http://localhost/*",
+ "menus",
+ "notifications",
+ "storage",
+ "tabs"
+ ],
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ },
+ "compose_action": {
+ "default_title": "LaTeX It!",
+ "default_icon": "icon.png",
+ "type": "menu"
+ },
+ "commands": {
+ "tblatex-run-silent": {
+ "description": "Run LaTeX It! silently in compose editor",
+ "suggested_key": {
+ "default": "Ctrl+Shift+L",
+ "mac": "Command+Shift+L"
+ }
}
+ },
+ "options_ui": {
+ "page": "ui/options.html",
+ "open_in_tab": true
+ },
+ "experiment_apis": {
+ "TBLatex": {
+ "schema": "api/TBLatex/schema.json",
+ "parent": {
+ "scopes": [
+ "addon_parent"
+ ],
+ "paths": [
+ [
+ "TBLatex"
+ ]
+ ],
+ "script": "api/TBLatex/implementation.js"
+ }
+ }
+ },
+ "icons": {
+ "32": "icon.png"
}
}
diff --git a/scripts/build_xpi.sh b/scripts/build_xpi.sh
new file mode 100755
index 0000000..6cb0844
--- /dev/null
+++ b/scripts/build_xpi.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+OUT_FILE="${1:-tblatex.xpi}"
+ADDON_ID="${2:-}"
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
+BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/tblatex-build.XXXXXX")"
+
+cleanup() {
+ rm -rf "${BUILD_DIR}"
+}
+trap cleanup EXIT
+
+mkdir -p "${BUILD_DIR}"
+
+cp "${ROOT_DIR}/manifest.json" "${BUILD_DIR}/manifest.json"
+cp "${ROOT_DIR}/icon.png" "${BUILD_DIR}/icon.png"
+cp "${ROOT_DIR}/background.js" "${BUILD_DIR}/background.js"
+cp "${ROOT_DIR}/README.md" "${BUILD_DIR}/README.md"
+cp "${ROOT_DIR}/Changelog" "${BUILD_DIR}/Changelog"
+cp -R "${ROOT_DIR}/api" "${BUILD_DIR}/api"
+cp -R "${ROOT_DIR}/compose" "${BUILD_DIR}/compose"
+cp -R "${ROOT_DIR}/ui" "${BUILD_DIR}/ui"
+
+if [[ -n "${ADDON_ID}" ]]; then
+ node - "${BUILD_DIR}/manifest.json" "${ADDON_ID}" <<'NODE'
+const fs = require("fs");
+
+const manifestPath = process.argv[2];
+const addonId = process.argv[3];
+
+const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
+if (!manifest.applications || !manifest.applications.gecko) {
+ throw new Error("manifest.applications.gecko is missing");
+}
+
+manifest.applications.gecko.id = addonId;
+fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
+NODE
+fi
+
+rm -f "${ROOT_DIR}/${OUT_FILE}"
+(
+ cd "${BUILD_DIR}"
+ zip -r "${ROOT_DIR}/${OUT_FILE}" \
+ manifest.json \
+ icon.png \
+ background.js \
+ api \
+ compose \
+ ui \
+ README.md \
+ Changelog
+)
+
+echo "Built ${OUT_FILE}"
+if [[ -n "${ADDON_ID}" ]]; then
+ echo "Using add-on ID: ${ADDON_ID}"
+fi
diff --git a/skin/button.png b/skin/button.png
deleted file mode 100644
index 1599d19..0000000
Binary files a/skin/button.png and /dev/null differ
diff --git a/skin/buttonsmall.png b/skin/buttonsmall.png
deleted file mode 100644
index ebd6f6d..0000000
Binary files a/skin/buttonsmall.png and /dev/null differ
diff --git a/skin/overlay.css b/skin/overlay.css
deleted file mode 100644
index e2ff524..0000000
--- a/skin/overlay.css
+++ /dev/null
@@ -1,7 +0,0 @@
-#tblatex-button-1 {
- list-style-image: url("chrome://tblatex/skin/button.png");
-}
-
-[iconsize="small"] #tblatex-button-1 {
- list-style-image: url("chrome://tblatex/skin/buttonsmall.png");
-}
diff --git a/tblatex.xpi b/tblatex.xpi
new file mode 100644
index 0000000..2a5387b
Binary files /dev/null and b/tblatex.xpi differ
diff --git a/ui/insert.css b/ui/insert.css
new file mode 100644
index 0000000..f636e96
--- /dev/null
+++ b/ui/insert.css
@@ -0,0 +1,126 @@
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font: 14px/1.4 sans-serif;
+ background: #f5f5f9;
+ color: #111;
+}
+
+main {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 12px 16px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 0;
+}
+
+h1 {
+ margin-top: 0;
+ margin-bottom: 2px;
+}
+
+.dialog-content {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ padding-right: 2px;
+}
+
+textarea,
+input[type="number"],
+select {
+ width: 100%;
+ border: 1px solid #bcbcc4;
+ border-radius: 6px;
+ padding: 8px;
+ font: inherit;
+}
+
+textarea {
+ resize: vertical;
+ min-height: 220px;
+ flex: 1 1 auto;
+}
+
+.history {
+ margin: 0;
+}
+
+#formulaHistory {
+ min-height: 6.5em;
+}
+
+.row {
+ display: grid;
+ gap: 12px;
+ margin-top: 0;
+}
+
+.checkbox {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 0;
+ flex-wrap: wrap;
+}
+
+.compact-actions {
+ justify-content: flex-start;
+}
+
+.footer-actions {
+ justify-content: flex-end;
+ align-items: center;
+ border-top: 1px solid #ddd;
+ padding-top: 8px;
+ flex-wrap: nowrap;
+}
+
+button {
+ appearance: none;
+ border: 1px solid #6766e8;
+ background: #6766e8;
+ color: #fff;
+ border-radius: 6px;
+ padding: 8px 12px;
+ cursor: pointer;
+}
+
+#resetTemplate {
+ border-color: #666;
+ background: #666;
+}
+
+#refreshHistory {
+ border-color: #666;
+ background: #666;
+}
+
+#cancel {
+ border-color: #666;
+ background: #666;
+}
+
+#status {
+ min-height: 1.4em;
+ margin: 0;
+}
diff --git a/ui/insert.html b/ui/insert.html
new file mode 100644
index 0000000..d78144f
--- /dev/null
+++ b/ui/insert.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ Insert Complex LaTeX
+
+
+
+
+
Insert Complex LaTeX
+
+
+ Edit the LaTeX document below. The visible result will be inserted at the
+ current cursor position in the compose editor.
+
+
+ If a LaTeX image is selected in the compose body, this dialog edits and
+ replaces that formula in place.
+