Implement \verb (#614)

* Implement \verb

* Implement @gagern's comments

* \verb: look up characters one at a time.

* Add screenshot test for \verb

* Add error tests for \verb

* Include space symbol in typewriter font, and fix single quotes

This is based on https://github.com/Khan/MathJax-dev/pull/2
which hasn't been accepted yet at the time this commit is made.

* Add \verb* tests

* \verb should use Typewriter-Regular font!

* Switch \verb to use text mode and no-break space.

* Screenshot update with Typewriter-Regular

* \verb test: fix *, add commas to make spaces clear

* Fix spaces and style handling

* Implement @kevinbarabash's comments

* Make error clearly an assertion failure

* verb screenshot for Chrome
This commit is contained in:
Erik Demaine
2017-09-21 23:43:05 -04:00
committed by Kevin Barabash
parent c47655cc0e
commit f10ea4cbeb
16 changed files with 117 additions and 8 deletions

View File

@@ -986,11 +986,12 @@ $map{cmtt10} = {
0x15 => [0x306,-525,0], # \breve (combining)
0x16 => [0x304,-525,0], # \bar (combining)
0x17 => [0x30A,-525,0], # ring above (combining)
0x20 => 0x2423, # graphic representation of space
[0x21,0x7F] => 0x21,
0x27 => 2018, # left quote
0x60 => 2019, # right quote
0x27 => 0x2018, # left quote
0x60 => 0x2019, # right quote
0x5E => [0x302,-525,0], # \hat (combining)
0x7E => [0x303,-525,0], # \tilde (combining)
0x7F => [0x308,-525,0], # \ddot (combining)

View File

@@ -35,6 +35,8 @@ const tokenRegex = new RegExp(
"([ \r\n\t]+)|" + // whitespace
"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint
"|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair
"|\\\\verb\\*([^]).*?\\3" + // \verb*
"|\\\\verb([^*a-zA-Z]).*?\\4" + // \verb unstarred
"|\\\\(?:[a-zA-Z@]+|[^\uD800-\uDFFF])" + // function name
")"
);

View File

@@ -916,6 +916,25 @@ class Parser {
return new ParseFuncOrArgument(
nucleus.text,
false, nucleus);
} else if (/^\\verb[^a-zA-Z]/.test(nucleus.text)) {
this.consume();
let arg = nucleus.text.slice(5);
const star = (arg.charAt(0) === "*");
if (star) {
arg = arg.slice(1);
}
// Lexer's tokenRegex is constructed to always have matching
// first/last characters.
if (arg.length < 2 || arg.charAt(0) !== arg.slice(-1)) {
throw new ParseError(`\\verb assertion failed --
please report what input caused this bug`);
}
arg = arg.slice(1, -1); // remove first and last char
return new ParseFuncOrArgument(
new ParseNode("verb", {
body: arg,
star: star,
}, "text"), false, nucleus);
} else {
return null;
}

View File

@@ -168,6 +168,20 @@ const makeOrd = function(group, options, type) {
}
};
/**
* Combine as many characters as possible in the given array of characters
* via their tryCombine method.
*/
const tryCombineChars = function(chars) {
for (let i = 0; i < chars.length - 1; i++) {
if (chars[i].tryCombine(chars[i + 1])) {
chars.splice(i + 1, 1);
i--;
}
}
return chars;
};
/**
* Calculate the height, depth, and maxFontSize of an element based on its
* children.
@@ -394,6 +408,18 @@ const makeVList = function(children, positionType, positionData, options) {
return vtable;
};
// Converts verb group into body string, dealing with \verb* form
const makeVerb = function(group, options) {
let text = group.value.body;
if (group.value.star) {
text = text.replace(/ /g, '\u2423'); // Open Box
} else {
text = text.replace(/ /g, '\xA0'); // No-Break Space
// (so that, in particular, spaces don't coalesce)
}
return text;
};
// A map of spacing functions to their attributes, like size and corresponding
// CSS class
const spacingFunctions = {
@@ -487,6 +513,8 @@ module.exports = {
makeFragment: makeFragment,
makeVList: makeVList,
makeOrd: makeOrd,
makeVerb: makeVerb,
tryCombineChars: tryCombineChars,
prependChildren: prependChildren,
spacingFunctions: spacingFunctions,
};

View File

@@ -306,12 +306,7 @@ groupTypes.ordgroup = function(group, options) {
groupTypes.text = function(group, options) {
const newOptions = options.withFont(group.value.font);
const inner = buildExpression(group.value.body, newOptions, true);
for (let i = 0; i < inner.length - 1; i++) {
if (inner[i].tryCombine(inner[i + 1])) {
inner.splice(i + 1, 1);
i--;
}
}
buildCommon.tryCombineChars(inner);
return makeSpan(["mord", "text"],
inner, newOptions);
};
@@ -1094,6 +1089,30 @@ groupTypes.font = function(group, options) {
return buildGroup(group.value.body, options.withFont(font));
};
groupTypes.verb = function(group, options) {
const text = buildCommon.makeVerb(group, options);
const body = [];
// \verb enters text mode and therefore is sized like \textstyle
const newOptions = options.havingStyle(options.style.text());
for (let i = 0; i < text.length; i++) {
if (text[i] === '\xA0') { // spaces appear as nonbreaking space
// The space character isn't in the Typewriter-Regular font,
// so we implement it as a kern of the same size as a character.
// 0.525 is the width of a texttt character in LaTeX.
// It automatically gets scaled by the font size.
const rule = makeSpan(["mord", "rule"], [], newOptions);
rule.style.marginLeft = "0.525em";
body.push(rule);
} else {
body.push(buildCommon.makeSymbol(text[i], "Typewriter-Regular",
group.mode, newOptions, ["mathtt"]));
}
}
buildCommon.tryCombineChars(body);
return makeSpan(["mord", "text"].concat(newOptions.sizingClasses(options)),
body, newOptions);
};
groupTypes.rule = function(group, options) {
// Make an empty span for the rule
const rule = makeSpan(["mord", "rule"], [], options);

View File

@@ -455,6 +455,13 @@ groupTypes.sizing = function(group, options) {
return node;
};
groupTypes.verb = function(group, options) {
const text = new mathMLTree.TextNode(buildCommon.makeVerb(group, options));
const node = new mathMLTree.MathNode("mtext", [text]);
node.setAttribute("mathvariant", fontMap["mathtt"].variant);
return node;
};
groupTypes.overline = function(group, options) {
const operator = new mathMLTree.MathNode(
"mo", [new mathMLTree.TextNode("\u203e")]);

View File

@@ -1748,7 +1748,10 @@ const fontMetricsData = {
"937": [0, 0.61111, 0, 0],
"2018": [0, 0.61111, 0, 0],
"2019": [0, 0.61111, 0, 0],
"8216": [0, 0.61111, 0, 0],
"8217": [0, 0.61111, 0, 0],
"8242": [0, 0.61111, 0, 0],
"9251": [0.11111, 0.21944, 0, 0],
},
};

View File

@@ -713,3 +713,15 @@ defineFunction(["\\raisebox"], {
value: ordargument(body),
};
});
// \verb and \verb* are dealt with directly in Parser.js.
// If we end up here, it's because of a failure to match the two delimiters
// in the regex in Lexer.js. LaTeX raises the following error when \verb is
// terminated by end of line (or file).
defineFunction(["\\verb"], {
numArgs: 0,
allowedInText: true,
}, function(context) {
throw new ParseError(
"\\verb ended by end of line instead of matching delimiter");
});

View File

@@ -183,6 +183,17 @@ describe("Parser:", function() {
});
});
describe("#verb", function() {
it("complains about mismatched \\verb with end of string", function() {
expect("\\verb|hello").toFailWithParseError(
"\\verb ended by end of line instead of matching delimiter");
});
it("complains about mismatched \\verb with end of line", function() {
expect("\\verb|hello\nworld|").toFailWithParseError(
"\\verb ended by end of line instead of matching delimiter");
});
});
});
describe("Parser.expect calls:", function() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -279,6 +279,13 @@ UnsupportedCmds:
noThrow: 1
errorColor: "#dd4c4c"
nolatex: deliberately does not compile
Verb: |
\begin{array}{ll}
\verb \verb , & \verb|\verb |, \\
\verb* \verb* , & \verb*|\verb* |, \\
\verb!<x> & </y>! & \scriptstyle\verb|ss verb| \\
\verb*!<x> & </y>! & \small\verb|sm verb| \\
\end{array}
VerticalSpacing:
pre: potato<br>blah
tex: x^{\Huge y}z