\newcommand, \renewcommand, \providecommand (#1382)

* \newcommand, \renewcommand, \providecommand

* Tests

* Add comment

* Add symbols to the set of already defined things

* Add implicitCommands, catch \hline outside array

* Add \relax

* Move isDefined to be a method of MacroExpander

* Namespace.has

* Reword error messages

* Add \hdashline given #1407
This commit is contained in:
Erik Demaine
2018-06-07 13:39:39 +02:00
committed by GitHub
parent a5ef29fab1
commit 65569249be
9 changed files with 212 additions and 46 deletions

View File

@@ -4,6 +4,8 @@
* until only non-macro tokens remain. * until only non-macro tokens remain.
*/ */
import functions from "./functions";
import symbols from "./symbols";
import Lexer from "./Lexer"; import Lexer from "./Lexer";
import {Token} from "./Token"; import {Token} from "./Token";
import type {Mode} from "./types"; import type {Mode} from "./types";
@@ -15,6 +17,16 @@ import type {MacroContextInterface, MacroDefinition, MacroExpansion}
from "./macros"; from "./macros";
import type Settings from "./Settings"; import type Settings from "./Settings";
// List of commands that act like macros but aren't defined as a macro,
// function, or symbol. Used in `isDefined`.
export const implicitCommands = {
"\\relax": true, // MacroExpander.js
"^": true, // Parser.js
"_": true, // Parser.js
"\\limits": true, // Parser.js
"\\nolimits": true, // Parser.js
};
export default class MacroExpander implements MacroContextInterface { export default class MacroExpander implements MacroContextInterface {
maxExpand: number; maxExpand: number;
lexer: Lexer; lexer: Lexer;
@@ -316,5 +328,19 @@ export default class MacroExpander implements MacroContextInterface {
return expansion; return expansion;
} }
/**
* Determine whether a command is currently "defined" (has some
* functionality), meaning that it's a macro (in the current group),
* a function, a symbol, or one of the special commands listed in
* `implicitCommands`.
*/
isDefined(name: string): boolean {
return this.macros.has(name) ||
functions.hasOwnProperty(name) ||
symbols.math.hasOwnProperty(name) ||
symbols.text.hasOwnProperty(name) ||
implicitCommands.hasOwnProperty(name);
}
} }

View File

@@ -56,7 +56,21 @@ export default class Namespace<Value> {
} }
/** /**
* Get the current value of a name. * Detect whether `name` has a definition. Equivalent to
* `get(name) != null`.
*/
has(name: string): boolean {
return this.current.hasOwnProperty(name) ||
this.builtins.hasOwnProperty(name);
}
/**
* Get the current value of a name, or `undefined` if there is no value.
*
* Note: Do not use `if (namespace.get(...))` to detect whether a macro
* is defined, as the definition may be the empty string which evaluates
* to `false` in JavaScript. Use `if (namespace.get(...) != null)` or
* `if (namespace.has(...))`.
*/ */
get(name: string): ?Value { get(name: string): ?Value {
if (this.current.hasOwnProperty(name)) { if (this.current.hasOwnProperty(name)) {

View File

@@ -1,6 +1,7 @@
// @flow // @flow
import buildCommon from "../buildCommon"; import buildCommon from "../buildCommon";
import defineEnvironment from "../defineEnvironment"; import defineEnvironment from "../defineEnvironment";
import defineFunction from "../defineFunction";
import mathMLTree from "../mathMLTree"; import mathMLTree from "../mathMLTree";
import ParseError from "../ParseError"; import ParseError from "../ParseError";
import ParseNode from "../ParseNode"; import ParseNode from "../ParseNode";
@@ -659,3 +660,18 @@ defineEnvironment({
htmlBuilder, htmlBuilder,
mathmlBuilder, mathmlBuilder,
}); });
// Catch \hline outside array environment
defineFunction({
type: "text", // Doesn't matter what this is.
names: ["\\hline", "\\hdashline"],
props: {
numArgs: 0,
allowedInText: true,
allowedInMath: true,
},
handler(context, args) {
throw new ParseError(
`${context.funcName} valid only within array environment`);
},
});

View File

@@ -38,6 +38,11 @@ export interface MacroContextInterface {
*/ */
expandAfterFuture(): Token; expandAfterFuture(): Token;
/**
* Recursively expand first token, then return first non-expandable token.
*/
expandNextToken(): Token;
/** /**
* Fully expand the given macro name and return the resulting list of * Fully expand the given macro name and return the resulting list of
* tokens, or return `undefined` if no such macro is defined. * tokens, or return `undefined` if no such macro is defined.
@@ -55,6 +60,14 @@ export interface MacroContextInterface {
* and return the resulting array of arguments. * and return the resulting array of arguments.
*/ */
consumeArgs(numArgs: number): Token[][]; consumeArgs(numArgs: number): Token[][];
/**
* Determine whether a command is currently "defined" (has some
* functionality), meaning that it's a macro (in the current group),
* a function, a symbol, or one of the special commands listed in
* `implicitCommands`.
*/
isDefined(name: string): boolean;
} }
/** Macro tokens (in reverse order). */ /** Macro tokens (in reverse order). */
@@ -118,11 +131,12 @@ defineMacro("\\TextOrMath", function(context) {
} }
}); });
// Basic support for global macro definitions: // Basic support for macro definitions:
// \gdef\macro{expansion} // \def\macro{expansion}
// \gdef\macro#1{expansion} // \def\macro#1{expansion}
// \gdef\macro#1#2{expansion} // \def\macro#1#2{expansion}
// \gdef\macro#1#2#3#4#5#6#7#8#9{expansion} // \def\macro#1#2#3#4#5#6#7#8#9{expansion}
// Also the \gdef and \global\def equivalents
const def = (context, global: boolean) => { const def = (context, global: boolean) => {
let arg = context.consumeArgs(1)[0]; let arg = context.consumeArgs(1)[0];
if (arg.length !== 1) { if (arg.length !== 1) {
@@ -161,6 +175,7 @@ defineMacro("\\global", (context) => {
throw new ParseError("Invalid command after \\global"); throw new ParseError("Invalid command after \\global");
} }
const command = next[0].text; const command = next[0].text;
// TODO: Should expand command
if (command === "\\def") { if (command === "\\def") {
// \global\def is equivalent to \gdef // \global\def is equivalent to \gdef
return def(context, true); return def(context, true);
@@ -169,6 +184,55 @@ defineMacro("\\global", (context) => {
} }
}); });
// \newcommand{\macro}[args]{definition}
// \renewcommand{\macro}[args]{definition}
// TODO: Optional arguments: \newcommand{\macro}[args][default]{definition}
const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
let arg = context.consumeArgs(1)[0];
if (arg.length !== 1) {
throw new ParseError(
"\\newcommand's first argument must be a macro name");
}
const name = arg[0].text;
const exists = context.isDefined(name);
if (exists && !existsOK) {
throw new ParseError(`\\newcommand{${name}} attempting to redefine ` +
`${name}; use \\renewcommand`);
}
if (!exists && !nonexistsOK) {
throw new ParseError(`\\renewcommand{${name}} when command ${name} ` +
`does not yet exist; use \\newcommand`);
}
let numArgs = 0;
arg = context.consumeArgs(1)[0];
if (arg.length === 1 && arg[0].text === "[") {
let argText = '';
let token = context.expandNextToken();
while (token.text !== "]" && token.text !== "EOF") {
// TODO: Should properly expand arg, e.g., ignore {}s
argText += token.text;
token = context.expandNextToken();
}
if (!argText.match(/^\s*[0-9]+\s*$/)) {
throw new ParseError(`Invalid number of arguments: ${argText}`);
}
numArgs = parseInt(argText);
arg = context.consumeArgs(1)[0];
}
// Final arg is the expansion of the macro
context.macros.set(name, {
tokens: arg,
numArgs,
});
return '';
};
defineMacro("\\newcommand", (context) => newcommand(context, false, true));
defineMacro("\\renewcommand", (context) => newcommand(context, true, false));
defineMacro("\\providecommand", (context) => newcommand(context, true, true));
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Grouping // Grouping
// \let\bgroup={ \let\egroup=} // \let\bgroup={ \let\egroup=}

View File

@@ -5,15 +5,10 @@ import ParseError from "../src/ParseError";
import parseTree from "../src/parseTree"; import parseTree from "../src/parseTree";
import Settings from "../src/Settings"; import Settings from "../src/Settings";
export const defaultSettings = new Settings({ export const nonstrictSettings = new Settings({strict: false});
strict: false, // deal with warnings only when desired
});
export const strictSettings = new Settings({strict: true}); export const strictSettings = new Settings({strict: true});
export const _getBuilt = function(expr, settings = defaultSettings) { export const _getBuilt = function(expr, settings = new Settings()) {
if (settings === defaultSettings) {
settings.macros = {};
}
let rootNode = katex.__renderToDomTree(expr, settings); let rootNode = katex.__renderToDomTree(expr, settings);
if (rootNode.classes.indexOf('katex-error') >= 0) { if (rootNode.classes.indexOf('katex-error') >= 0) {
@@ -43,7 +38,7 @@ export const _getBuilt = function(expr, settings = defaultSettings) {
* @param settings * @param settings
* @returns {Object} * @returns {Object}
*/ */
export const getBuilt = function(expr, settings = defaultSettings) { export const getBuilt = function(expr, settings = new Settings()) {
expect(expr).toBuild(settings); expect(expr).toBuild(settings);
return _getBuilt(expr, settings); return _getBuilt(expr, settings);
}; };
@@ -54,7 +49,7 @@ export const getBuilt = function(expr, settings = defaultSettings) {
* @param settings * @param settings
* @returns {Object} * @returns {Object}
*/ */
export const getParsed = function(expr, settings = defaultSettings) { export const getParsed = function(expr, settings = new Settings()) {
expect(expr).toParse(settings); expect(expr).toParse(settings);
return parseTree(expr, settings); return parseTree(expr, settings);
}; };
@@ -73,7 +68,7 @@ export const stripPositions = function(expr) {
}; };
export const parseAndSetResult = function(expr, result, export const parseAndSetResult = function(expr, result,
settings = defaultSettings) { settings = new Settings()) {
try { try {
return parseTree(expr, settings); return parseTree(expr, settings);
} catch (e) { } catch (e) {
@@ -89,7 +84,7 @@ export const parseAndSetResult = function(expr, result,
}; };
export const buildAndSetResult = function(expr, result, export const buildAndSetResult = function(expr, result,
settings = defaultSettings) { settings = new Settings()) {
try { try {
return _getBuilt(expr, settings); return _getBuilt(expr, settings);
} catch (e) { } catch (e) {

View File

@@ -11,7 +11,7 @@ import Options from "../src/Options";
import Settings from "../src/Settings"; import Settings from "../src/Settings";
import Style from "../src/Style"; import Style from "../src/Style";
import { import {
defaultSettings, strictSettings, strictSettings, nonstrictSettings,
_getBuilt, getBuilt, getParsed, stripPositions, _getBuilt, getBuilt, getParsed, stripPositions,
} from "./helpers"; } from "./helpers";
@@ -1140,6 +1140,10 @@ describe("A begin/end parser", function() {
expect("\\begin{matrix}\\hdashline a&b\\\\ \\hdashline c&d\\end{matrix}").toParse(); expect("\\begin{matrix}\\hdashline a&b\\\\ \\hdashline c&d\\end{matrix}").toParse();
}); });
it("should forbid hlines outside array environment", () => {
expect("\\hline").toNotParse();
});
it("should error when name is mismatched", function() { it("should error when name is mismatched", function() {
expect("\\begin{matrix}a&b\\\\c&d\\end{pmatrix}").toNotParse(); expect("\\begin{matrix}a&b\\\\c&d\\end{pmatrix}").toNotParse();
}); });
@@ -2282,7 +2286,7 @@ describe("A smash builder", function() {
describe("A parser error", function() { describe("A parser error", function() {
it("should report the position of an error", function() { it("should report the position of an error", function() {
try { try {
parseTree("\\sqrt}", defaultSettings); parseTree("\\sqrt}", new Settings());
} catch (e) { } catch (e) {
expect(e.position).toEqual(5); expect(e.position).toEqual(5);
} }
@@ -2490,7 +2494,7 @@ describe("A macro expander", function() {
const compareParseTree = function(actual, expected, macros) { const compareParseTree = function(actual, expected, macros) {
const settings = new Settings({macros: macros}); const settings = new Settings({macros: macros});
actual = stripPositions(parseTree(actual, settings)); actual = stripPositions(parseTree(actual, settings));
expected = stripPositions(parseTree(expected, defaultSettings)); expected = stripPositions(parseTree(expected, new Settings()));
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}; };
@@ -2775,6 +2779,57 @@ describe("A macro expander", function() {
expect(macros["\\foo"]).toBeFalsy(); expect(macros["\\foo"]).toBeFalsy();
}); });
it("\\newcommand defines new macros", () => {
compareParseTree("\\newcommand\\foo{x^2}\\foo+\\foo", "x^2+x^2");
compareParseTree("\\newcommand{\\foo}{x^2}\\foo+\\foo", "x^2+x^2");
// Function detection
expect("\\newcommand\\bar{x^2}\\bar+\\bar").toNotParse();
expect("\\newcommand{\\bar}{x^2}\\bar+\\bar").toNotParse();
// Symbol detection
expect("\\newcommand\\lambda{x^2}\\lambda").toNotParse();
expect("\\newcommand\\textdollar{x^2}\\textdollar").toNotParse();
// Macro detection
expect("\\newcommand{\\foo}{1}\\foo\\newcommand{\\foo}{2}\\foo")
.toNotParse();
// Implicit detection
expect("\\newcommand\\limits{}").toNotParse();
});
it("\\renewcommand redefines macros", () => {
expect("\\renewcommand\\foo{x^2}\\foo+\\foo").toNotParse();
expect("\\renewcommand{\\foo}{x^2}\\foo+\\foo").toNotParse();
compareParseTree("\\renewcommand\\bar{x^2}\\bar+\\bar", "x^2+x^2");
compareParseTree("\\renewcommand{\\bar}{x^2}\\bar+\\bar", "x^2+x^2");
expect("\\newcommand{\\foo}{1}\\foo\\renewcommand{\\foo}{2}\\foo")
.toParseLike("12");
});
it("\\providecommand (re)defines macros", () => {
compareParseTree("\\providecommand\\foo{x^2}\\foo+\\foo", "x^2+x^2");
compareParseTree("\\providecommand{\\foo}{x^2}\\foo+\\foo", "x^2+x^2");
compareParseTree("\\providecommand\\bar{x^2}\\bar+\\bar", "x^2+x^2");
compareParseTree("\\providecommand{\\bar}{x^2}\\bar+\\bar", "x^2+x^2");
expect("\\newcommand{\\foo}{1}\\foo\\providecommand{\\foo}{2}\\foo")
.toParseLike("12");
expect("\\providecommand{\\foo}{1}\\foo\\renewcommand{\\foo}{2}\\foo")
.toParseLike("12");
expect("\\providecommand{\\foo}{1}\\foo\\providecommand{\\foo}{2}\\foo")
.toParseLike("12");
});
it("\\newcommand is local", () => {
expect("\\newcommand\\foo{1}\\foo{\\renewcommand\\foo{2}\\foo}\\foo")
.toParseLike("1{2}1");
});
it("\\newcommand accepts number of arguments", () => {
compareParseTree("\\newcommand\\foo[1]{#1^2}\\foo x+\\foo{y}",
"x^2+y^2");
compareParseTree("\\newcommand\\foo[10]{#1^2}\\foo 0123456789", "0^2");
expect("\\newcommand\\foo[x]{}").toNotParse();
expect("\\newcommand\\foo[1.5]{}").toNotParse();
});
// This may change in the future, if we support the extra features of // This may change in the future, if we support the extra features of
// \hspace. // \hspace.
it("should treat \\hspace, \\hskip like \\kern", function() { it("should treat \\hspace, \\hskip like \\kern", function() {
@@ -2847,7 +2902,7 @@ describe("Unicode accents", function() {
"\\tilde n" + "\\tilde n" +
"\\grave o\\acute o\\hat o\\tilde o\\ddot o" + "\\grave o\\acute o\\hat o\\tilde o\\ddot o" +
"\\grave u\\acute u\\hat u\\ddot u" + "\\grave u\\acute u\\hat u\\ddot u" +
"\\acute y\\ddot y"); "\\acute y\\ddot y", nonstrictSettings);
}); });
it("should parse Latin-1 letters in text mode", function() { it("should parse Latin-1 letters in text mode", function() {
@@ -2877,19 +2932,19 @@ describe("Unicode accents", function() {
}); });
it("should parse combining characters", function() { it("should parse combining characters", function() {
expect("A\u0301C\u0301").toParseLike("Á\\acute C"); expect("A\u0301C\u0301").toParseLike("Á\\acute C", nonstrictSettings);
expect("\\text{A\u0301C\u0301}").toParseLike("\\text{Á\\'C}", strictSettings); expect("\\text{A\u0301C\u0301}").toParseLike("\\text{Á\\'C}", strictSettings);
}); });
it("should parse multi-accented characters", function() { it("should parse multi-accented characters", function() {
expect("ấā́ắ\\text{ấā́ắ}").toParse(); expect("ấā́ắ\\text{ấā́ắ}").toParse(nonstrictSettings);
// Doesn't parse quite the same as // Doesn't parse quite the same as
// "\\text{\\'{\\^a}\\'{\\=a}\\'{\\u a}}" because of the ordgroups. // "\\text{\\'{\\^a}\\'{\\=a}\\'{\\u a}}" because of the ordgroups.
}); });
it("should parse accented i's and j's", function() { it("should parse accented i's and j's", function() {
expect("íȷ́").toParseLike("\\acute ı\\acute ȷ"); expect("íȷ́").toParseLike("\\acute ı\\acute ȷ", nonstrictSettings);
expect("ấā́ắ\\text{ấā́ắ}").toParse(); expect("ấā́ắ\\text{ấā́ắ}").toParse(nonstrictSettings);
}); });
}); });
@@ -3076,8 +3131,8 @@ describe("Symbols", function() {
describe("strict setting", function() { describe("strict setting", function() {
it("should allow unicode text when not strict", () => { it("should allow unicode text when not strict", () => {
expect("é").toParse(new Settings({strict: false})); expect("é").toParse(new Settings(nonstrictSettings));
expect("試").toParse(new Settings({strict: false})); expect("試").toParse(new Settings(nonstrictSettings));
expect("é").toParse(new Settings({strict: "ignore"})); expect("é").toParse(new Settings({strict: "ignore"}));
expect("試").toParse(new Settings({strict: "ignore"})); expect("試").toParse(new Settings({strict: "ignore"}));
expect("é").toParse(new Settings({strict: () => false})); expect("é").toParse(new Settings({strict: () => false}));
@@ -3103,7 +3158,7 @@ describe("strict setting", function() {
}); });
it("should always allow unicode text in text mode", () => { it("should always allow unicode text in text mode", () => {
expect("\\text{é試}").toParse(new Settings({strict: false})); expect("\\text{é試}").toParse(nonstrictSettings);
expect("\\text{é試}").toParse(strictSettings); expect("\\text{é試}").toParse(strictSettings);
expect("\\text{é試}").toParse(); expect("\\text{é試}").toParse();
}); });

View File

@@ -8,13 +8,9 @@ import Options from "../src/Options";
import Settings from "../src/Settings"; import Settings from "../src/Settings";
import Style from "../src/Style"; import Style from "../src/Style";
const defaultSettings = new Settings({}); const getMathML = function(expr, settings = new Settings()) {
const getMathML = function(expr, settings) {
const usedSettings = settings ? settings : defaultSettings;
let startStyle = Style.TEXT; let startStyle = Style.TEXT;
if (usedSettings.displayMode) { if (settings.displayMode) {
startStyle = Style.DISPLAY; startStyle = Style.DISPLAY;
} }
@@ -24,7 +20,7 @@ const getMathML = function(expr, settings) {
maxSize: Infinity, maxSize: Infinity,
}); });
const built = buildMathML(parseTree(expr, usedSettings), expr, options); const built = buildMathML(parseTree(expr, settings), expr, options);
// Strip off the surrounding <span> // Strip off the surrounding <span>
return built.children[0].toMarkup(); return built.children[0].toMarkup();

View File

@@ -4,10 +4,10 @@
import katex from "../katex"; import katex from "../katex";
import ParseError from "../src/ParseError"; import ParseError from "../src/ParseError";
import parseTree from "../src/parseTree"; import parseTree from "../src/parseTree";
import Settings from "../src/Settings";
import Warning from "./Warning"; import Warning from "./Warning";
import stringify from 'json-stable-stringify'; import stringify from 'json-stable-stringify';
import { import {
defaultSettings,
_getBuilt, buildAndSetResult, parseAndSetResult, stripPositions, _getBuilt, buildAndSetResult, parseAndSetResult, stripPositions,
} from "./helpers"; } from "./helpers";
@@ -44,7 +44,7 @@ global.console.warn = jest.fn((warning) => {
// Expect extensions // Expect extensions
expect.extend({ expect.extend({
toParse: function(actual, settings = defaultSettings) { toParse: function(actual, settings = new Settings()) {
const result = { const result = {
pass: true, pass: true,
message: () => `'${actual}' succeeded parsing`, message: () => `'${actual}' succeeded parsing`,
@@ -53,7 +53,7 @@ expect.extend({
return result; return result;
}, },
toNotParse: function(actual, settings = defaultSettings) { toNotParse: function(actual, settings = new Settings()) {
const result = { const result = {
pass: false, pass: false,
message: () => message: () =>
@@ -79,7 +79,7 @@ expect.extend({
toFailWithParseError: function(actual, expected) { toFailWithParseError: function(actual, expected) {
const prefix = "KaTeX parse error: "; const prefix = "KaTeX parse error: ";
try { try {
parseTree(actual, defaultSettings); parseTree(actual, new Settings());
return { return {
pass: false, pass: false,
message: () => `'${actual}' parsed without error`, message: () => `'${actual}' parsed without error`,
@@ -115,7 +115,7 @@ expect.extend({
} }
}, },
toBuild: function(actual, settings = defaultSettings) { toBuild: function(actual, settings = new Settings()) {
const result = { const result = {
pass: true, pass: true,
message: () => `'${actual}' succeeded in building`, message: () => `'${actual}' succeeded in building`,
@@ -124,7 +124,7 @@ expect.extend({
return result; return result;
}, },
toNotBuild: function(actual, settings = defaultSettings) { toNotBuild: function(actual, settings = new Settings()) {
const result = { const result = {
pass: false, pass: false,
message: () => message: () =>
@@ -147,7 +147,7 @@ expect.extend({
return result; return result;
}, },
toParseLike: function(actual, expected, settings = defaultSettings) { toParseLike: function(actual, expected, settings = new Settings()) {
const result = { const result = {
pass: true, pass: true,
message: () => message: () =>
@@ -174,7 +174,7 @@ expect.extend({
return result; return result;
}, },
toBuildLike: function(actual, expected, settings = defaultSettings) { toBuildLike: function(actual, expected, settings = new Settings()) {
const result = { const result = {
pass: true, pass: true,
message: () => message: () =>
@@ -201,7 +201,7 @@ expect.extend({
return result; return result;
}, },
toWarn: function(actual, settings = defaultSettings) { toWarn: function(actual, settings = new Settings()) {
const result = { const result = {
pass: false, pass: false,
message: () => message: () =>

View File

@@ -4,7 +4,7 @@
/* global describe: false */ /* global describe: false */
import Settings from "../src/Settings"; import Settings from "../src/Settings";
import {scriptFromCodepoint, supportedCodepoint} from "../src/unicodeScripts"; import {scriptFromCodepoint, supportedCodepoint} from "../src/unicodeScripts";
import {strictSettings} from "./helpers"; import {strictSettings, nonstrictSettings} from "./helpers";
describe("unicode", function() { describe("unicode", function() {
it("should parse Latin-1 inside \\text{}", function() { it("should parse Latin-1 inside \\text{}", function() {
@@ -21,7 +21,7 @@ describe("unicode", function() {
it("should parse Latin-1 outside \\text{}", function() { it("should parse Latin-1 outside \\text{}", function() {
expect('ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ' + expect('ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ' +
'ÇÐÞçðþ').toParse(); 'ÇÐÞçðþ').toParse(nonstrictSettings);
}); });
it("should parse all lower case Greek letters", function() { it("should parse all lower case Greek letters", function() {