Skip to content

Instantly share code, notes, and snippets.

@etihwnad
Last active May 6, 2023 00:16
Show Gist options
  • Save etihwnad/bc63ec9b87af586e1435 to your computer and use it in GitHub Desktop.
Save etihwnad/bc63ec9b87af586e1435 to your computer and use it in GitHub Desktop.
Render LaTeX math in the web interface of Slack. Wrap code in $$ ... $$
{
"manifest_version": 2,
"name": "SlackTeX",
"description": "This extension loads MathJax in the web verson of Slack, allowing the nice rendering of LaTeX math code. Text wrapped with $ ... $ is rendered as inline math and text wrapped with either $$ ... $$ or \\[ ... \\] is considered displaymath.",
"version": "1.2",
"browser_action": {
"default_title": "SlackTeX",
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
],
"content_scripts": [
{
"matches": ["https://*.slack.com/*"],
"js": ["mathjax-slack.user.js"]
}
]
}
// ==UserScript==
// @name SlackTeX
// @namespace http://www.whiteaudio.com/
// @author Dan White <apps@whiteaudio.com>
// @version 1.2
// @description Enables MathJax to process LaTeX on Slack's web interface.
// @match https://*.slack.com/*
// @copyright
// ==/UserScript==
// original base:
// https://gist.github.com/goatandsheep/c8bf7b4ae448e76208a0/raw/76d36877a82d34fccf6e17e63c3db3cbef3712c8/Texify-Mathjax.js
var mathjax_config = document.createElement("script");
mathjax_config.type = "text/x-mathjax-config";
mathjax_config.text = 'MathJax.Hub.Config({ ' +
'tex2jax: { ' +
'inlineMath: [ ["$","$"] ],' +
'displayMath: [ ["\[","\]"], ["$$","$$"] ],' +
'processEscapes: true },' +
' }); ' +
'MathJax.Hub.Startup.onload(); ' +
'MathJax.Hub.Queue(["Typeset", MathJax.Hub]);';
document.getElementsByTagName("head")[0].appendChild(mathjax_config);
var mathjax_script = document.createElement("script");
mathjax_script.type = "text/javascript";
mathjax_script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML";
document.getElementsByTagName("head")[0].appendChild(mathjax_script);
// observe changes to the msgs_div and convo_scroller IDs (added messages) and fire another MathJax pass
var render = function (records, observer) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
};
var messages_pane = document.querySelector('#msgs_div');
var flex_pane = document.querySelector('#convo_scroller');
var messages_observer = new MutationObserver(render);
var flex_observer = new MutationObserver(render);
var config = { attributes: false, childList: true, characterData: true, subtree: true};
messages_observer.observe(messages_pane, config);
flex_observer.observe(flex_pane, config);
<!doctype html>
<html>
<head>
<title>SlackTeX Options</title>
<style>
body {
font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
font-size: 100%;
}
#status {
/* avoid an excessively wide status text */
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
}
</style>
</head>
<body>
SlackTeX is an extension that renders LaTeX-formatted mathematics found in Slack messages.
<p>
Inline math delimiters: <input type="text" id=="inlinemath" name="inlinemath"><br>
Display math delimiters: <input type="text" id=="displaymath" name="displaymath">
<div id="status"></div>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>
function save_options() {
var inlinemath = document.getElementById('inlinemath').value;
var displaymath = document.getElementById('displaymath').value;
chrome.storage.sync.set({
inlinemath: inlinemath,
displaymath: displaymath
}, function() {
// Update status to let user know options were saved.
var status = document.getElementById('status');
status.textContent = 'Options saved.';
setTimeout(function() {
status.textContent = '';
}, 750);
});
}
// Restores select box and checkbox state using the preferences
// stored in chrome.storage.
function restore_options() {
// Use default value color = 'red' and likesColor = true.
chrome.storage.sync.get({
inlinemath: "$,$",
displaymath: "\[,\]"
}, function(items) {
document.getElementById('inlinemath').value = items.inlinemath;
document.getElementById('displaymath').value = items.displaymath;
});
}
document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click', save_options);
<!doctype html>
<!--
This page is shown when the extension button is clicked, because the
"browser_action" field in manifest.json contains the "default_popup" key with
value "popup.html".
-->
<html>
<head>
<title>Getting Started Extension's Popup</title>
<style>
body {
font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
font-size: 100%;
}
#status {
/* avoid an excessively wide status text */
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
}
</style>
<!--
- JavaScript and HTML must be in separate files: see our Content Security
- Policy documentation[1] for details and explanation.
-
- [1]: https://developer.chrome.com/extensions/contentSecurityPolicy
-->
<script src="popup.js"></script>
</head>
<body>
SlackTeX is an extension that renders LaTeX-formatted mathematics found in Slack messages.
</body>
</html>
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Get the current URL.
*
* @param {function(string)} callback - called when the URL of the current tab
* is found.
*/
function getCurrentTabUrl(callback) {
// Query filter to be passed to chrome.tabs.query - see
// https://developer.chrome.com/extensions/tabs#method-query
var queryInfo = {
active: true,
currentWindow: true
};
chrome.tabs.query(queryInfo, function(tabs) {
// chrome.tabs.query invokes the callback with a list of tabs that match the
// query. When the popup is opened, there is certainly a window and at least
// one tab, so we can safely assume that |tabs| is a non-empty array.
// A window can only have one active tab at a time, so the array consists of
// exactly one tab.
var tab = tabs[0];
// A tab is a plain object that provides information about the tab.
// See https://developer.chrome.com/extensions/tabs#type-Tab
var url = tab.url;
// tab.url is only available if the "activeTab" permission is declared.
// If you want to see the URL of other tabs (e.g. after removing active:true
// from |queryInfo|), then the "tabs" permission is required to see their
// "url" properties.
console.assert(typeof url == 'string', 'tab.url should be a string');
callback(url);
});
// Most methods of the Chrome extension APIs are asynchronous. This means that
// you CANNOT do something like this:
//
// var url;
// chrome.tabs.query(queryInfo, function(tabs) {
// url = tabs[0].url;
// });
// alert(url); // Shows "undefined", because chrome.tabs.query is async.
}
/**
* @param {string} searchTerm - Search term for Google Image search.
* @param {function(string,number,number)} callback - Called when an image has
* been found. The callback gets the URL, width and height of the image.
* @param {function(string)} errorCallback - Called when the image is not found.
* The callback gets a string that describes the failure reason.
*/
function getImageUrl(searchTerm, callback, errorCallback) {
// Google image search - 100 searches per day.
// https://developers.google.com/image-search/
var searchUrl = 'https://ajax.googleapis.com/ajax/services/search/images' +
'?v=1.0&q=' + encodeURIComponent(searchTerm);
var x = new XMLHttpRequest();
x.open('GET', searchUrl);
// The Google image search API responds with JSON, so let Chrome parse it.
x.responseType = 'json';
x.onload = function() {
// Parse and process the response from Google Image Search.
var response = x.response;
if (!response || !response.responseData || !response.responseData.results ||
response.responseData.results.length === 0) {
errorCallback('No response from Google Image search!');
return;
}
var firstResult = response.responseData.results[0];
// Take the thumbnail instead of the full image to get an approximately
// consistent image size.
var imageUrl = firstResult.tbUrl;
var width = parseInt(firstResult.tbWidth);
var height = parseInt(firstResult.tbHeight);
console.assert(
typeof imageUrl == 'string' && !isNaN(width) && !isNaN(height),
'Unexpected respose from the Google Image Search API!');
callback(imageUrl, width, height);
};
x.onerror = function() {
errorCallback('Network error.');
};
x.send();
}
function renderStatus(statusText) {
document.getElementById('status').textContent = statusText;
}
document.addEventListener('DOMContentLoaded', function() {
getCurrentTabUrl(function(url) {
// Put the image URL in Google search.
renderStatus('Performing Google Image search for ' + url);
getImageUrl(url, function(imageUrl, width, height) {
renderStatus('Search term: ' + url + '\n' +
'Google image search result: ' + imageUrl);
var imageResult = document.getElementById('image-result');
// Explicitly set the width/height to minimize the number of reflows. For
// a single image, this does not matter, but if you're going to embed
// multiple external images in your page, then the absence of width/height
// attributes causes the popup to resize multiple times.
imageResult.width = width;
imageResult.height = height;
imageResult.src = imageUrl;
imageResult.hidden = false;
}, function(errorMessage) {
renderStatus('Cannot display image. ' + errorMessage);
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment