mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-06 03:38:39 +00:00
Support \includegraphics (#1620)
* Support \includegraphics * Update screenshots * Fix lint errors * Fix more lint errors * Fix lint errors take 3 * Add documentation, error messages * Improve CSS * Update domTree to coordinate with PR #1633 * Update screenshots * fix flow errors * Use same RegEx as href * Fix lint error * Accept raw ArgType * Fix lint error * fix flow error * Add test
This commit is contained in:
@@ -474,7 +474,7 @@ table td {
|
|||||||
|\impliedby|$P\impliedby Q$|`P\impliedby Q`|
|
|\impliedby|$P\impliedby Q$|`P\impliedby Q`|
|
||||||
|\implies|$P\implies Q$|`P\implies Q`|
|
|\implies|$P\implies Q$|`P\implies Q`|
|
||||||
|\in|$\in$||
|
|\in|$\in$||
|
||||||
|\includegraphics|<span style="color:firebrick;">Not supported</span>|[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$||
|
|\inf|$\inf$||
|
||||||
|\infin|$\infin$||
|
|\infin|$\infin$||
|
||||||
|\infty|$\infty$||
|
|\infty|$\infty$||
|
||||||
|
@@ -108,6 +108,10 @@ The `{array}` environment does not yet support `\cline` or `\multicolumn`.
|
|||||||
|:----------------|:-------------------|
|
|:----------------|:-------------------|
|
||||||
| $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` |
|
| $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` |
|
||||||
| $\url{https://katex.org/}$ | `\url{https://katex.org/}` |
|
| $\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
|
## Letters and Unicode
|
||||||
|
|
||||||
|
@@ -527,6 +527,18 @@ export default class Parser {
|
|||||||
case "math":
|
case "math":
|
||||||
case "text":
|
case "text":
|
||||||
return this.parseGroup(name, optional, greediness, undefined, type);
|
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 "original":
|
||||||
case null:
|
case null:
|
||||||
case undefined:
|
case undefined:
|
||||||
|
@@ -264,6 +264,69 @@ export class Anchor implements HtmlDomNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This node represents an image embed (<img>) 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 = `<img src='${this.src} 'alt='${this.alt}' `;
|
||||||
|
|
||||||
|
// Add the styles, after hyphenation
|
||||||
|
let styles = "";
|
||||||
|
for (const style in this.style) {
|
||||||
|
if (this.style.hasOwnProperty(style)) {
|
||||||
|
styles += `${utils.hyphenate(style)}:${this.style[style]};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (styles) {
|
||||||
|
markup += ` style="${utils.escape(styles)}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
markup += "'/>";
|
||||||
|
return markup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const iCombinations = {
|
const iCombinations = {
|
||||||
'î': '\u0131\u0302',
|
'î': '\u0131\u0302',
|
||||||
'ï': '\u0131\u0308',
|
'ï': '\u0131\u0308',
|
||||||
|
@@ -21,6 +21,7 @@ import "./functions/genfrac";
|
|||||||
import "./functions/horizBrace";
|
import "./functions/horizBrace";
|
||||||
import "./functions/href";
|
import "./functions/href";
|
||||||
import "./functions/htmlmathml";
|
import "./functions/htmlmathml";
|
||||||
|
import "./functions/includegraphics";
|
||||||
import "./functions/kern";
|
import "./functions/kern";
|
||||||
import "./functions/lap";
|
import "./functions/lap";
|
||||||
import "./functions/math";
|
import "./functions/math";
|
||||||
|
146
src/functions/includegraphics.js
Normal file
146
src/functions/includegraphics.js
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
});
|
@@ -464,6 +464,14 @@
|
|||||||
stroke-opacity: 1;
|
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.
|
// Define CSS for image whose width will match its span width.
|
||||||
.stretchy {
|
.stretchy {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -25,7 +25,7 @@ export type MathNodeType =
|
|||||||
"mfrac" | "mroot" | "msqrt" |
|
"mfrac" | "mroot" | "msqrt" |
|
||||||
"mtable" | "mtr" | "mtd" | "mlabeledtr" |
|
"mtable" | "mtr" | "mtd" | "mlabeledtr" |
|
||||||
"mrow" | "menclose" |
|
"mrow" | "menclose" |
|
||||||
"mstyle" | "mpadded" | "mphantom";
|
"mstyle" | "mpadded" | "mphantom" | "mglyph";
|
||||||
|
|
||||||
export interface MathDomNode extends VirtualNode {
|
export interface MathDomNode extends VirtualNode {
|
||||||
toText(): string;
|
toText(): string;
|
||||||
|
@@ -49,6 +49,12 @@ type ParseNodeTypes = {
|
|||||||
loc?: ?SourceLocation,
|
loc?: ?SourceLocation,
|
||||||
color: string,
|
color: string,
|
||||||
|},
|
|},
|
||||||
|
"keyVals": {|
|
||||||
|
type: "keyVals",
|
||||||
|
mode: Mode,
|
||||||
|
loc?: ?SourceLocation,
|
||||||
|
keyVals: string,
|
||||||
|
|},
|
||||||
// To avoid requiring run-time type assertions, this more carefully captures
|
// To avoid requiring run-time type assertions, this more carefully captures
|
||||||
// the requirements on the fields per the op.js htmlBuilder logic:
|
// the requirements on the fields per the op.js htmlBuilder logic:
|
||||||
// - `body` and `value` are NEVER set simultanouesly.
|
// - `body` and `value` are NEVER set simultanouesly.
|
||||||
@@ -271,6 +277,16 @@ type ParseNodeTypes = {
|
|||||||
html: AnyParseNode[],
|
html: AnyParseNode[],
|
||||||
mathml: AnyParseNode[],
|
mathml: AnyParseNode[],
|
||||||
|},
|
|},
|
||||||
|
"includegraphics": {|
|
||||||
|
type: "includegraphics",
|
||||||
|
mode: Mode,
|
||||||
|
loc?: ?SourceLocation,
|
||||||
|
alt: string,
|
||||||
|
width: Measurement,
|
||||||
|
height: Measurement,
|
||||||
|
totalheight: Measurement,
|
||||||
|
src: string,
|
||||||
|
|},
|
||||||
"infix": {|
|
"infix": {|
|
||||||
type: "infix",
|
type: "infix",
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
@@ -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() {
|
describe("A bin builder", function() {
|
||||||
it("should create mbins normally", function() {
|
it("should create mbins normally", function() {
|
||||||
const built = getBuilt`x + y`;
|
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() {
|
describe("A parser that does not throw on unsupported commands", function() {
|
||||||
// The parser breaks on unsupported commands unless it is explicitly
|
// The parser breaks on unsupported commands unless it is explicitly
|
||||||
// told not to
|
// told not to
|
||||||
|
BIN
test/screenshotter/images/Includegraphics-chrome.png
Normal file
BIN
test/screenshotter/images/Includegraphics-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
test/screenshotter/images/Includegraphics-firefox.png
Normal file
BIN
test/screenshotter/images/Includegraphics-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
@@ -137,6 +137,13 @@ GroupMacros:
|
|||||||
\endExp: \egroup
|
\endExp: \egroup
|
||||||
tex: \startExp a+b\endExp
|
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}}
|
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: |
|
Integrands: |
|
||||||
\begin{array}{l}
|
\begin{array}{l}
|
||||||
\displaystyle \int + \oint + \iint + \oiint_i^n \\[0.6em]
|
\displaystyle \int + \oint + \iint + \oiint_i^n \\[0.6em]
|
||||||
|
Reference in New Issue
Block a user