diff --git a/docs/support_table.md b/docs/support_table.md
index 928adba9..25762bf6 100644
--- a/docs/support_table.md
+++ b/docs/support_table.md
@@ -474,7 +474,7 @@ table td {
|\impliedby|$P\impliedby Q$|`P\impliedby Q`|
|\implies|$P\implies Q$|`P\implies Q`|
|\in|$\in$||
-|\includegraphics|Not supported|[Issue #898](https://github.com/Khan/KaTeX/issues/898)|
+|\includegraphics|$\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}$|`\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}`|
|\inf|$\inf$||
|\infin|$\infin$||
|\infty|$\infty$||
diff --git a/docs/supported.md b/docs/supported.md
index 2d69441c..cbbcbbf4 100644
--- a/docs/supported.md
+++ b/docs/supported.md
@@ -108,6 +108,10 @@ The `{array}` environment does not yet support `\cline` or `\multicolumn`.
|:----------------|:-------------------|
| $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` |
| $\url{https://katex.org/}$ | `\url{https://katex.org/}` |
+| $\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}$ | `\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}` |
+
+`\includegraphics` supports`height`, `width`, `totalheight`, and `alt` in it's first argument. `height` is required.
+
## Letters and Unicode
diff --git a/src/Parser.js b/src/Parser.js
index 9616bc44..2b42d386 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -527,6 +527,18 @@ export default class Parser {
case "math":
case "text":
return this.parseGroup(name, optional, greediness, undefined, type);
+ case "raw": {
+ const token = this.parseStringGroup("raw", optional, true);
+ if (token) {
+ return {
+ type: "raw",
+ mode: "text",
+ string: token.text,
+ };
+ } else {
+ throw new ParseError("Expected raw group", this.nextToken);
+ }
+ }
case "original":
case null:
case undefined:
diff --git a/src/domTree.js b/src/domTree.js
index 426d5562..38ffd912 100644
--- a/src/domTree.js
+++ b/src/domTree.js
@@ -264,6 +264,69 @@ export class Anchor implements HtmlDomNode {
}
}
+/**
+ * This node represents an image embed (
) element.
+ */
+export class Img implements VirtualNode {
+ src: string;
+ alt: string;
+ classes: string[];
+ height: number;
+ depth: number;
+ maxFontSize: number;
+ style: CssStyle;
+
+ constructor(
+ src: string,
+ alt: string,
+ style: CssStyle,
+ ) {
+ this.alt = alt;
+ this.src = src;
+ this.classes = ["mord"];
+ this.style = style;
+ }
+
+ hasClass(className: string): boolean {
+ return utils.contains(this.classes, className);
+ }
+
+ toNode(): Node {
+ const node = document.createElement("img");
+ node.src = this.src;
+ node.alt = this.alt;
+ node.className = "mord";
+
+ // Apply inline styles
+ for (const style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ // $FlowFixMe
+ node.style[style] = this.style[style];
+ }
+ }
+
+ return node;
+ }
+
+ toMarkup(): string {
+ let markup = `
";
+ return markup;
+ }
+}
+
const iCombinations = {
'î': '\u0131\u0302',
'ï': '\u0131\u0308',
diff --git a/src/functions.js b/src/functions.js
index 07af852b..5bfae0eb 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -21,6 +21,7 @@ import "./functions/genfrac";
import "./functions/horizBrace";
import "./functions/href";
import "./functions/htmlmathml";
+import "./functions/includegraphics";
import "./functions/kern";
import "./functions/lap";
import "./functions/math";
diff --git a/src/functions/includegraphics.js b/src/functions/includegraphics.js
new file mode 100644
index 00000000..70cb427b
--- /dev/null
+++ b/src/functions/includegraphics.js
@@ -0,0 +1,146 @@
+// @flow
+import defineFunction from "../defineFunction";
+import type {Measurement} from "../units";
+import {calculateSize, validUnit} from "../units";
+import ParseError from "../ParseError";
+import {Img} from "../domTree";
+import mathMLTree from "../mathMLTree";
+import {assertNodeType} from "../parseNode";
+import type {CssStyle} from "../domTree";
+
+const sizeData = function(str: string): Measurement {
+ if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) {
+ // str is a number with no unit specified.
+ // default unit is bp, per graphix package.
+ return {number: +str, unit: "bp"};
+ } else {
+ const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(str);
+ if (!match) {
+ throw new ParseError("Invalid size: '" + str
+ + "' in \\includegraphics");
+ }
+ const data = {
+ number: +(match[1] + match[2]), // sign + magnitude, cast to number
+ unit: match[3],
+ };
+ if (!validUnit(data)) {
+ throw new ParseError("Invalid unit: '" + data.unit
+ + "' in \\includegraphics.");
+ }
+ return data;
+ }
+};
+
+defineFunction({
+ type: "includegraphics",
+ names: ["\\includegraphics"],
+ props: {
+ numArgs: 1,
+ numOptionalArgs: 1,
+ argTypes: ["raw", "url"],
+ allowedInText: false,
+ },
+ handler: ({parser}, args, optArgs) => {
+ let width = {number: 0, unit: "em"};
+ let height = {number: 0.9, unit: "em"}; // sorta character sized.
+ let totalheight = {number: 0, unit: "em"};
+ let alt = "";
+
+ if (optArgs[0]) {
+ const attributeStr = assertNodeType(optArgs[0], "raw").string;
+
+ // Parser.js does not parse key/value pairs. We get a string.
+ const attributes = attributeStr.split(",");
+ for (let i = 0; i < attributes.length; i++) {
+ const keyVal = attributes[i].split("=");
+ if (keyVal.length === 2) {
+ const str = keyVal[1].trim();
+ switch (keyVal[0].trim()) {
+ case "alt":
+ alt = str;
+ break;
+ case "width":
+ width = sizeData(str);
+ break;
+ case "height":
+ height = sizeData(str);
+ break;
+ case "totalheight":
+ totalheight = sizeData(str);
+ break;
+ default:
+ throw new ParseError("Invalid key: '" + keyVal[0] +
+ "' in \\includegraphics.");
+ }
+ }
+ }
+ }
+
+ const src = assertNodeType(args[0], "url").url;
+
+ if (alt === "") {
+ // No alt given. Use the file name. Strip away the path.
+ alt = src;
+ alt = alt.replace(/^.*[\\/]/, '');
+ alt = alt.substring(0, alt.lastIndexOf('.'));
+ }
+
+ return {
+ type: "includegraphics",
+ mode: parser.mode,
+ alt: alt,
+ width: width,
+ height: height,
+ totalheight: totalheight,
+ src: src,
+ };
+ },
+ htmlBuilder: (group, options) => {
+ const height = calculateSize(group.height, options);
+ let depth = 0;
+
+ if (group.totalheight.number > 0) {
+ depth = calculateSize(group.totalheight, options) - height;
+ depth = Number(depth.toFixed(2));
+ }
+
+ let width = 0;
+ if (group.width.number > 0) {
+ width = calculateSize(group.width, options);
+ }
+
+ const style: CssStyle = {height: height + depth + "em"};
+ if (width > 0) {
+ style.width = width + "em";
+ }
+ if (depth > 0) {
+ style.verticalAlign = -depth + "em";
+ }
+
+ const node = new Img(group.src, group.alt, style);
+ node.height = height;
+ node.depth = depth;
+
+ return node;
+ },
+ mathmlBuilder: (group, options) => {
+ const node = new mathMLTree.MathNode("mglyph", []);
+ node.setAttribute("alt", group.alt);
+
+ const height = calculateSize(group.height, options);
+ let depth = 0;
+ if (group.totalheight.number > 0) {
+ depth = calculateSize(group.totalheight, options) - height;
+ depth = depth.toFixed(2);
+ node.setAttribute("valign", "-" + depth + "em");
+ }
+ node.setAttribute("height", height + depth + "em");
+
+ if (group.width.number > 0) {
+ const width = calculateSize(group.width, options);
+ node.setAttribute("width", width + "em");
+ }
+ node.setAttribute("src", group.src);
+ return node;
+ },
+});
diff --git a/src/katex.less b/src/katex.less
index c26e1f9c..aaebebf9 100644
--- a/src/katex.less
+++ b/src/katex.less
@@ -464,6 +464,14 @@
stroke-opacity: 1;
}
+ img {
+ border-style: none;
+ min-width: 0;
+ min-height: 0;
+ max-width: none;
+ max-height: none;
+ }
+
// Define CSS for image whose width will match its span width.
.stretchy {
width: 100%;
diff --git a/src/mathMLTree.js b/src/mathMLTree.js
index 971d1a99..69ecc849 100644
--- a/src/mathMLTree.js
+++ b/src/mathMLTree.js
@@ -25,7 +25,7 @@ export type MathNodeType =
"mfrac" | "mroot" | "msqrt" |
"mtable" | "mtr" | "mtd" | "mlabeledtr" |
"mrow" | "menclose" |
- "mstyle" | "mpadded" | "mphantom";
+ "mstyle" | "mpadded" | "mphantom" | "mglyph";
export interface MathDomNode extends VirtualNode {
toText(): string;
diff --git a/src/parseNode.js b/src/parseNode.js
index b6e8897e..87b9c919 100644
--- a/src/parseNode.js
+++ b/src/parseNode.js
@@ -49,6 +49,12 @@ type ParseNodeTypes = {
loc?: ?SourceLocation,
color: string,
|},
+ "keyVals": {|
+ type: "keyVals",
+ mode: Mode,
+ loc?: ?SourceLocation,
+ keyVals: string,
+ |},
// To avoid requiring run-time type assertions, this more carefully captures
// the requirements on the fields per the op.js htmlBuilder logic:
// - `body` and `value` are NEVER set simultanouesly.
@@ -271,6 +277,16 @@ type ParseNodeTypes = {
html: AnyParseNode[],
mathml: AnyParseNode[],
|},
+ "includegraphics": {|
+ type: "includegraphics",
+ mode: Mode,
+ loc?: ?SourceLocation,
+ alt: string,
+ width: Measurement,
+ height: Measurement,
+ totalheight: Measurement,
+ src: string,
+ |},
"infix": {|
type: "infix",
mode: Mode,
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 773b48a6..b5804e42 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -1947,6 +1947,17 @@ describe("A MathML font tree-builder", function() {
});
});
+describe("An includegraphics builder", function() {
+ const img = "\\includegraphics[height=0.9em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}";
+ it("should not fail", function() {
+ expect(img).toBuild();
+ });
+
+ it("should produce mords", function() {
+ expect(getBuilt(img)[0].classes).toContain("mord");
+ });
+});
+
describe("A bin builder", function() {
it("should create mbins normally", function() {
const built = getBuilt`x + y`;
@@ -2647,6 +2658,15 @@ describe("href and url commands", function() {
});
});
+describe("A raw text parser", function() {
+ it("should not not parse a mal-formed string", function() {
+ // In the next line, the first character passed to \includegraphics is a
+ // Unicode combining character. So this is a test that the parser will catch a bad string.
+ expect("\\includegraphics[\u030aheight=0.8em, totalheight=0.9em, width=0.9em]{" + "https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}").not.toParse();
+ });
+});
+
+
describe("A parser that does not throw on unsupported commands", function() {
// The parser breaks on unsupported commands unless it is explicitly
// told not to
diff --git a/test/screenshotter/images/Includegraphics-chrome.png b/test/screenshotter/images/Includegraphics-chrome.png
new file mode 100644
index 00000000..038a3862
Binary files /dev/null and b/test/screenshotter/images/Includegraphics-chrome.png differ
diff --git a/test/screenshotter/images/Includegraphics-firefox.png b/test/screenshotter/images/Includegraphics-firefox.png
new file mode 100644
index 00000000..6fb6ba2e
Binary files /dev/null and b/test/screenshotter/images/Includegraphics-firefox.png differ
diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml
index 34e55889..8d9b9d5b 100644
--- a/test/screenshotter/ss_data.yaml
+++ b/test/screenshotter/ss_data.yaml
@@ -137,6 +137,13 @@ GroupMacros:
\endExp: \egroup
tex: \startExp a+b\endExp
HorizontalBraces: \overbrace{\displaystyle{\oint_S{\vec E\cdot\hat n\,\mathrm d a}}}^\text{emf} = \underbrace{\frac{q_{\text{enc}}}{\varepsilon_0}}_{\text{charge}}
+Includegraphics: |
+ \def\logo{\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}}
+ \def\logoB{\includegraphics[height=0.4em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}}
+ \begin{array}{l}
+ \underline{A\logo} + \sqrt{\logo} + \tfrac{A\logo}{\logo}\\[1em]
+ \underline{A\logoB} + \sqrt{x\logoB} + \tfrac{A\logoB}{\logoB}
+ \end{array}
Integrands: |
\begin{array}{l}
\displaystyle \int + \oint + \iint + \oiint_i^n \\[0.6em]