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:
Ron Kok
2018-11-01 15:43:41 -07:00
committed by Kevin Barabash
parent f628ca142b
commit f713f324bd
13 changed files with 279 additions and 2 deletions

View File

@@ -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:

View File

@@ -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 = {
'î': '\u0131\u0302',
'ï': '\u0131\u0308',

View File

@@ -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";

View 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;
},
});

View File

@@ -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%;

View File

@@ -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;

View File

@@ -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,