Implement strict mode (replacing unicodeTextInMathMode) (#1278)

* Implement strict mode (replacing unicodeTextInMathMode)

Add new "strict" setting (default value false) that can take a boolean
(whether to throw an error or silently ignore), string ("ignore",
"warn", or "error"), or a function possibly returning such a value.
This enables a variety of ways of handling or ignoring transgressions
from "true" LaTeX behavior, making KaTeX easier to use while still
providing the ability for strict LaTeX adherance.

Resolve #1226, implementing that spec, for two existing
transgressions from regular LaTeX:

* src/functions/kern.js had some errors and warnings about use of
  (units in) math vs. text mode commands.
* The former setting unicodeTextInMathMode (not in any released version)
  needed to be set to true to enable Unicode text symbols in math mode.

Now these are controlled by the strict setting.  By default, KaTeX is now
very permissive, but if desired, the user can request warnings or errors.

* Rewrite strict description

* Add tests for strict functions

* Stricter type for strict

* Switch default strict setting to "warn"

* Fix new flow error

* Fix another flow bug
This commit is contained in:
Erik Demaine
2018-05-13 14:27:30 -04:00
committed by GitHub
parent 4801ab875a
commit 7ab4f76e16
10 changed files with 208 additions and 79 deletions

20
test/Warning.js Normal file
View File

@@ -0,0 +1,20 @@
// @flow
class Warning {
name: string;
message: string;
stack: string;
constructor(message: string) {
// $FlowFixMe
this.name = "Warning";
// $FlowFixMe
this.message = "Warning: " + message;
// $FlowFixMe
this.stack = new Error().stack;
}
}
// $FlowFixMe
Warning.prototype = Object.create(Error.prototype);
module.exports = Warning;

View File

@@ -35,7 +35,9 @@ const serializer = {
expect.addSnapshotSerializer(serializer);
const defaultSettings = new Settings({});
const defaultSettings = new Settings({
strict: false, // deal with warnings only when desired
});
const defaultOptions = new Options({
style: Style.TEXT,
size: 5,
@@ -2928,24 +2930,23 @@ describe("A parser taking String objects", function() {
describe("Unicode accents", function() {
it("should parse Latin-1 letters in math mode", function() {
// TODO(edemaine): Unsupported Latin-1 letters in math: ÅåÇÐÞçðþ
expect("ÀÁÂÃÄÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäèéêëìíîïñòóôõöùúûüýÿ")
// TODO(edemaine): Unsupported Latin-1 letters in math: ÇÐÞçðþ
expect("ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ")
.toParseLike(
"\\grave A\\acute A\\hat A\\tilde A\\ddot A" +
"\\grave A\\acute A\\hat A\\tilde A\\ddot A\\mathring A" +
"\\grave E\\acute E\\hat E\\ddot E" +
"\\grave I\\acute I\\hat I\\ddot I" +
"\\tilde N" +
"\\grave O\\acute O\\hat O\\tilde O\\ddot O" +
"\\grave U\\acute U\\hat U\\ddot U" +
"\\acute Y" +
"\\grave a\\acute a\\hat a\\tilde a\\ddot a" +
"\\grave a\\acute a\\hat a\\tilde a\\ddot a\\mathring a" +
"\\grave e\\acute e\\hat e\\ddot e" +
"\\grave ı\\acute ı\\hat ı\\ddot ı" +
"\\tilde n" +
"\\grave o\\acute o\\hat o\\tilde o\\ddot o" +
"\\grave u\\acute u\\hat u\\ddot u" +
"\\acute y\\ddot y",
{unicodeTextInMathMode: true});
"\\acute y\\ddot y");
});
it("should parse Latin-1 letters in text mode", function() {
@@ -2970,26 +2971,24 @@ describe("Unicode accents", function() {
it("should support \\aa in text mode", function() {
expect("\\text{\\aa\\AA}").toParseLike("\\text{\\r a\\r A}");
expect("\\aa").toNotParse();
expect("\\Aa").toNotParse();
expect("\\aa").toNotParse(new Settings({strict: true}));
expect("\\Aa").toNotParse(new Settings({strict: true}));
});
it("should parse combining characters", function() {
expect("A\u0301C\u0301").toParseLike("Á\\acute C",
{unicodeTextInMathMode: true});
expect("A\u0301C\u0301").toParseLike("Á\\acute C");
expect("\\text{A\u0301C\u0301}").toParseLike("\\text{Á\\'C}");
});
it("should parse multi-accented characters", function() {
expect("ấā́ắ\\text{ấā́ắ}").toParse({unicodeTextInMathMode: true});
expect("ấā́ắ\\text{ấā́ắ}").toParse();
// Doesn't parse quite the same as
// "\\text{\\'{\\^a}\\'{\\=a}\\'{\\u a}}" because of the ordgroups.
});
it("should parse accented i's and j's", function() {
expect("íȷ́").toParseLike("\\acute ı\\acute ȷ",
{unicodeTextInMathMode: true});
expect("ấā́ắ\\text{ấā́ắ}").toParse({unicodeTextInMathMode: true});
expect("íȷ́").toParseLike("\\acute ı\\acute ȷ");
expect("ấā́ắ\\text{ấā́ắ}").toParse();
});
});
@@ -3154,25 +3153,37 @@ describe("Symbols", function() {
});
});
describe("unicodeTextInMathMode setting", function() {
it("should allow unicode text when true", () => {
expect("é").toParse({unicodeTextInMathMode: true});
expect("試").toParse({unicodeTextInMathMode: true});
describe("strict setting", function() {
it("should allow unicode text when not strict", () => {
expect("é").toParse(new Settings({strict: false}));
expect("試").toParse(new Settings({strict: false}));
expect("é").toParse(new Settings({strict: "ignore"}));
expect("試").toParse(new Settings({strict: "ignore"}));
expect("é").toParse(new Settings({strict: () => false}));
expect("試").toParse(new Settings({strict: () => false}));
expect("é").toParse(new Settings({strict: () => "ignore"}));
expect("試").toParse(new Settings({strict: () => "ignore"}));
});
it("should forbid unicode text when false", () => {
expect("é").toNotParse({unicodeTextInMathMode: false});
expect("試").toNotParse({unicodeTextInMathMode: false});
it("should forbid unicode text when strict", () => {
expect("é").toNotParse(new Settings({strict: true}));
expect("試").toNotParse(new Settings({strict: true}));
expect("é").toNotParse(new Settings({strict: "error"}));
expect("試").toNotParse(new Settings({strict: "error"}));
expect("é").toNotParse(new Settings({strict: () => true}));
expect("試").toNotParse(new Settings({strict: () => true}));
expect("é").toNotParse(new Settings({strict: () => "error"}));
expect("試").toNotParse(new Settings({strict: () => "error"}));
});
it("should forbid unicode text when default", () => {
expect("é").toNotParse();
expect("試").toNotParse();
it("should warn about unicode text when default", () => {
expect("é").toWarn(new Settings());
expect("試").toWarn(new Settings());
});
it("should always allow unicode text in text mode", () => {
expect("\\text{é試}").toParse({unicodeTextInMathMode: false});
expect("\\text{é試}").toParse({unicodeTextInMathMode: true});
expect("\\text{é試}").toParse(new Settings({strict: false}));
expect("\\text{é試}").toParse(new Settings({strict: true}));
expect("\\text{é試}").toParse();
});
});

41
test/setup.js Normal file
View File

@@ -0,0 +1,41 @@
/* global jest: false */
/* global expect: false */
import katex from "../katex";
import Settings from "../src/Settings";
import Warning from "./Warning";
global.console.warn = jest.fn((warning) => {
throw new Warning(warning);
});
const defaultSettings = new Settings({
strict: false, // enable dealing with warnings only when needed
});
expect.extend({
toWarn: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = {
pass: false,
message: () =>
`Expected '${actual}' to generate a warning, but it succeeded`,
};
try {
katex.__renderToDomTree(actual, usedSettings);
} catch (e) {
if (e instanceof Warning) {
result.pass = true;
result.message = () =>
`'${actual}' correctly generated warning: ${e.message}`;
} else {
result.message = () =>
`'${actual}' failed building with unknown error: ${e.message}`;
}
}
return result;
},
});

View File

@@ -8,7 +8,10 @@ import parseTree from "../src/parseTree";
import Settings from "../src/Settings";
import {scriptFromCodepoint, supportedCodepoint} from "../src/unicodeScripts";
const defaultSettings = new Settings({});
const defaultSettings = new Settings({
strict: false, // deal with warnings only when desired
});
const strictSettings = new Settings({strict: true});
const parseAndSetResult = function(expr, result, settings) {
try {
@@ -72,16 +75,16 @@ describe("unicode", function() {
'ÆÇÐØÞßæçðøþ}').toParse();
});
it("should not parse Latin-1 outside \\text{} without setting", function() {
it("should not parse Latin-1 outside \\text{} with strict", function() {
const chars = 'ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿÇÐÞçþ';
for (const ch of chars) {
expect(ch).toNotParse();
expect(ch).toNotParse(strictSettings);
}
});
it("should parse Latin-1 outside \\text{}", function() {
expect('ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ' +
'ÇÐÞçðþ').toParse({unicodeTextInMathMode: true});
'ÇÐÞçðþ').toParse();
});
it("should parse all lower case Greek letters", function() {
@@ -96,8 +99,8 @@ describe("unicode", function() {
expect('\\text{БГДЖЗЙЛФЦШЫЮЯ}').toParse();
});
it("should not parse Cyrillic outside \\text{}", function() {
expect('БГДЖЗЙЛФЦШЫЮЯ').toNotParse();
it("should not parse Cyrillic outside \\text{} with strict", function() {
expect('БГДЖЗЙЛФЦШЫЮЯ').toNotParse(strictSettings);
});
it("should parse CJK inside \\text{}", function() {
@@ -105,33 +108,33 @@ describe("unicode", function() {
expect('\\text{여보세요}').toParse();
});
it("should not parse CJK outside \\text{}", function() {
expect('私はバナナです。').toNotParse();
expect('여보세요').toNotParse();
it("should not parse CJK outside \\text{} with strict", function() {
expect('私はバナナです。').toNotParse(strictSettings);
expect('여보세요').toNotParse(strictSettings);
});
it("should parse Devangari inside \\text{}", function() {
expect('\\text{नमस्ते}').toParse();
});
it("should not parse Devangari outside \\text{}", function() {
expect('नमस्ते').toNotParse();
it("should not parse Devangari outside \\text{} with strict", function() {
expect('नमस्ते').toNotParse(strictSettings);
});
it("should parse Georgian inside \\text{}", function() {
expect('\\text{გამარჯობა}').toParse();
});
it("should not parse Georgian outside \\text{}", function() {
expect('გამარჯობა').toNotParse();
it("should not parse Georgian outside \\text{} with strict", function() {
expect('გამარჯობა').toNotParse(strictSettings);
});
it("should parse extended Latin characters inside \\text{}", function() {
expect('\\text{ěščřžůřťďňőİı}').toParse();
});
it("should not parse extended Latin outside \\text{}", function() {
expect('ěščřžůřťďňőİı').toNotParse();
it("should not parse extended Latin outside \\text{} with strict", function() {
expect('ěščřžůřťďňőİı').toNotParse(strictSettings);
});
});