From 6857689946c50cb4a020d0214dc810913b8c9758 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 5 Sep 2017 09:27:04 +0900 Subject: [PATCH] Advanced macro support and magic \dots (#794) * Advanced macro support and magic \dots * Fix \relax behavior * Use \DOTSB in \iff, \implies, \impliedby * Add multiple expansion test * Implement some of @kevinbarash's comments * More @kevinbarabash comments * Token moved from merge * Add type to defineMacro * @flow --- src/Lexer.js | 6 +- src/MacroExpander.js | 264 ++++++++++++++------- src/macros.js | 165 ++++++++++++- src/symbols.js | 2 +- test/katex-spec.js | 61 +++++ test/screenshotter/images/Dots-chrome.png | Bin 0 -> 7858 bytes test/screenshotter/images/Dots-firefox.png | Bin 0 -> 6998 bytes test/screenshotter/ss_data.yaml | 5 + 8 files changed, 406 insertions(+), 97 deletions(-) create mode 100644 test/screenshotter/images/Dots-chrome.png create mode 100644 test/screenshotter/images/Dots-firefox.png diff --git a/src/Lexer.js b/src/Lexer.js index 341989f4..716e7631 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -35,12 +35,12 @@ const tokenRegex = new RegExp( "([ \r\n\t]+)|" + // whitespace "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair - "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name + "|\\\\(?:[a-zA-Z@]+|[^\uD800-\uDFFF])" + // function name ")" ); /** Main Lexer class */ -class Lexer implements LexerInterface { +export default class Lexer implements LexerInterface { input: string; pos: number; @@ -71,5 +71,3 @@ class Lexer implements LexerInterface { return new Token(text, start, end, this); } } - -module.exports = Lexer; diff --git a/src/MacroExpander.js b/src/MacroExpander.js index 38882207..9b5ee4c1 100644 --- a/src/MacroExpander.js +++ b/src/MacroExpander.js @@ -4,6 +4,7 @@ */ import Lexer from "./Lexer"; +import {Token} from "./Token"; import builtinMacros from "./macros"; import ParseError from "./ParseError"; import objectAssign from "object-assign"; @@ -17,114 +18,205 @@ class MacroExpander { } /** - * Recursively expand first token, then return first non-expandable token. + * Returns the topmost token on the stack, without expanding it. + * Similar in behavior to TeX's `\futurelet`. + */ + future() { + if (this.stack.length === 0) { + this.stack.push(this.lexer.lex()); + } + return this.stack[this.stack.length - 1]; + } + + /** + * Remove and return the next unexpanded token. + */ + popToken() { + this.future(); // ensure non-empty stack + return this.stack.pop(); + } + + /** + * Consume all following space tokens, without expansion. + */ + consumeSpaces() { + for (;;) { + const token = this.future(); + if (token.text === " ") { + this.stack.pop(); + } else { + break; + } + } + } + + /** + * Expand the next token only once if possible. + * + * If the token is expanded, the resulting tokens will be pushed onto + * the stack in reverse order and will be returned as an array, + * also in reverse order. + * + * If not, the next token will be returned without removing it + * from the stack. This case can be detected by a `Token` return value + * instead of an `Array` return value. + * + * In either case, the next token will be on the top of the stack, + * or the stack will be empty. + * + * Used to implement `expandAfterFuture` and `expandNextToken`. * * At the moment, macro expansion doesn't handle delimited macros, * i.e. things like those defined by \def\foo#1\end{…}. * See the TeX book page 202ff. for details on how those should behave. */ - nextToken() { - for (;;) { - if (this.stack.length === 0) { - this.stack.push(this.lexer.lex()); - } - const topToken = this.stack.pop(); - const name = topToken.text; - if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { - return topToken; - } - let tok; - let expansion = this.macros[name]; - if (typeof expansion === "string") { - let numArgs = 0; - if (expansion.indexOf("#") !== -1) { - const stripped = expansion.replace(/##/g, ""); - while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { - ++numArgs; - } + expandOnce() { + const topToken = this.popToken(); + const name = topToken.text; + const isMacro = (name.charAt(0) === "\\"); + if (isMacro) { + // Consume all spaces after \macro + this.consumeSpaces(); + } + if (!(isMacro && this.macros.hasOwnProperty(name))) { + // Fully expanded + this.stack.push(topToken); + return topToken; + } + let expansion = this.macros[name]; + if (typeof expansion === "function") { + expansion = expansion.call(this); + } + if (typeof expansion === "string") { + let numArgs = 0; + if (expansion.indexOf("#") !== -1) { + const stripped = expansion.replace(/##/g, ""); + while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { + ++numArgs; } - const bodyLexer = new Lexer(expansion); - expansion = []; + } + const bodyLexer = new Lexer(expansion); + expansion = []; + let tok = bodyLexer.lex(); + while (tok.text !== "EOF") { + expansion.push(tok); tok = bodyLexer.lex(); - while (tok.text !== "EOF") { - expansion.push(tok); - tok = bodyLexer.lex(); - } - expansion.reverse(); // to fit in with stack using push and pop - expansion.numArgs = numArgs; - this.macros[name] = expansion; } - if (expansion.numArgs) { - const args = []; - let i; - // obtain arguments, either single token or balanced {…} group - for (i = 0; i < expansion.numArgs; ++i) { - const startOfArg = this.get(true); - if (startOfArg.text === "{") { - const arg = []; - let depth = 1; - while (depth !== 0) { - tok = this.get(false); - arg.push(tok); - if (tok.text === "{") { - ++depth; - } else if (tok.text === "}") { - --depth; - } else if (tok.text === "EOF") { - throw new ParseError( - "End of input in macro argument", - startOfArg); - } + expansion.reverse(); // to fit in with stack using push and pop + expansion.numArgs = numArgs; + // TODO: Could cache macro expansions if it originally came as a + // String (but not those that come in as a Function). + } + if (expansion.numArgs) { + const args = []; + // obtain arguments, either single token or balanced {…} group + for (let i = 0; i < expansion.numArgs; ++i) { + this.consumeSpaces(); // ignore spaces before each argument + const startOfArg = this.popToken(); + if (startOfArg.text === "{") { + const arg = []; + let depth = 1; + while (depth !== 0) { + const tok = this.popToken(); + arg.push(tok); + if (tok.text === "{") { + ++depth; + } else if (tok.text === "}") { + --depth; + } else if (tok.text === "EOF") { + throw new ParseError( + "End of input in macro argument", + startOfArg); } - arg.pop(); // remove last } - arg.reverse(); // like above, to fit in with stack order - args[i] = arg; - } else if (startOfArg.text === "EOF") { + } + arg.pop(); // remove last } + arg.reverse(); // like above, to fit in with stack order + args[i] = arg; + } else if (startOfArg.text === "EOF") { + throw new ParseError( + "End of input expecting macro argument", topToken); + } else { + args[i] = [startOfArg]; + } + } + // paste arguments in place of the placeholders + expansion = expansion.slice(); // make a shallow copy + for (let i = expansion.length - 1; i >= 0; --i) { + let tok = expansion[i]; + if (tok.text === "#") { + if (i === 0) { throw new ParseError( - "End of input expecting macro argument", topToken); - } else { - args[i] = [startOfArg]; + "Incomplete placeholder at end of macro body", + tok); } - } - // paste arguments in place of the placeholders - expansion = expansion.slice(); // make a shallow copy - for (i = expansion.length - 1; i >= 0; --i) { - tok = expansion[i]; - if (tok.text === "#") { - if (i === 0) { - throw new ParseError( - "Incomplete placeholder at end of macro body", - tok); - } - tok = expansion[--i]; // next token on stack - if (tok.text === "#") { // ## → # - expansion.splice(i + 1, 1); // drop first # - } else if (/^[1-9]$/.test(tok.text)) { - // expansion.splice(i, 2, arg[0], arg[1], …) - // to replace placeholder with the indicated argument. - // TODO: use spread once we move to ES2015 - expansion.splice.apply( - expansion, - [i, 2].concat(args[tok.text - 1])); - } else { - throw new ParseError( - "Not a valid argument number", - tok); - } + tok = expansion[--i]; // next token on stack + if (tok.text === "#") { // ## → # + expansion.splice(i + 1, 1); // drop first # + } else if (/^[1-9]$/.test(tok.text)) { + // expansion.splice(i, 2, arg[0], arg[1], …) + // to replace placeholder with the indicated argument. + // TODO: use spread once we move to ES2015 + expansion.splice.apply( + expansion, + [i, 2].concat(args[tok.text - 1])); + } else { + throw new ParseError( + "Not a valid argument number", + tok); } } } - this.stack = this.stack.concat(expansion); + } + // Concatenate expansion onto top of stack. + this.stack.push.apply(this.stack, expansion); + return expansion; + } + + /** + * Expand the next token only once (if possible), and return the resulting + * top token on the stack (without removing anything from the stack). + * Similar in behavior to TeX's `\expandafter\futurelet`. + * Equivalent to expandOnce() followed by future(). + */ + expandAfterFuture() { + this.expandOnce(); + return this.future(); + } + + /** + * Recursively expand first token, then return first non-expandable token. + */ + expandNextToken() { + for (;;) { + const expanded = this.expandOnce(); + // expandOnce returns Token if and only if it's fully expanded. + if (expanded instanceof Token) { + // \relax stops the expansion, but shouldn't get returned (a + // null return value couldn't get implemented as a function). + if (expanded.text === "\\relax") { + this.stack.pop(); + } else { + return this.stack.pop(); // === expanded + } + } } } + /** + * Recursively expand first token, then return first non-expandable token. + * If given a `true` argument, skips over any leading whitespace in + * expansion, instead returning the first non-whitespace token + * (like TeX's \ignorespaces). + * Any skipped whitespace is stored in `this.discardedWhiteSpace` + * so that `unget` can correctly undo the effects of `get`. + */ get(ignoreSpace) { this.discardedWhiteSpace = []; - let token = this.nextToken(); + let token = this.expandNextToken(); if (ignoreSpace) { while (token.text === " ") { this.discardedWhiteSpace.push(token); - token = this.nextToken(); + token = this.expandNextToken(); } } return token; diff --git a/src/macros.js b/src/macros.js index 63ef710e..f290d40e 100644 --- a/src/macros.js +++ b/src/macros.js @@ -1,10 +1,14 @@ +// @flow /** * Predefined macros for KaTeX. * This can be used to define some commands in terms of others. */ +import symbols from "./symbols"; +import utils from "./utils"; + // This function might one day accept additional argument and do more things. -function defineMacro(name, body) { +function defineMacro(name: string, body: string | () => string) { module.exports[name] = body; } @@ -26,6 +30,7 @@ defineMacro("\\clap", "\\mathclap{\\textrm{#1}}"); ////////////////////////////////////////////////////////////////////// // amsmath.sty +// http://mirrors.concertpass.com/tex-archive/macros/latex/required/amsmath/amsmath.pdf // \def\overset#1#2{\binrel@{#2}\binrel@@{\mathop{\kern\z@#2}\limits^{#1}}} defineMacro("\\overset", "\\mathop{#2}\\limits^{#1}"); @@ -34,14 +39,162 @@ defineMacro("\\underset", "\\mathop{#2}\\limits_{#1}"); // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} defineMacro("\\boxed", "\\fbox{\\displaystyle{#1}}"); -//TODO: When implementing \dots, should ideally add the \DOTSB indicator -// into the macro, to indicate these are binary operators. // \def\iff{\DOTSB\;\Longleftrightarrow\;} // \def\implies{\DOTSB\;\Longrightarrow\;} // \def\impliedby{\DOTSB\;\Longleftarrow\;} -defineMacro("\\iff", "\\;\\Longleftrightarrow\\;"); -defineMacro("\\implies", "\\;\\Longrightarrow\\;"); -defineMacro("\\impliedby", "\\;\\Longleftarrow\\;"); +defineMacro("\\iff", "\\DOTSB\\;\\Longleftrightarrow\\;"); +defineMacro("\\implies", "\\DOTSB\\;\\Longrightarrow\\;"); +defineMacro("\\impliedby", "\\DOTSB\\;\\Longleftarrow\\;"); + +// AMSMath's automatic \dots, based on \mdots@@ macro. +const dotsByToken = { + ',': '\\dotsc', + '\\not': '\\dotsb', + // \keybin@ checks for the following: + '+': '\\dotsb', + '=': '\\dotsb', + '<': '\\dotsb', + '>': '\\dotsb', + '-': '\\dotsb', + '*': '\\dotsb', + ':': '\\dotsb', + // Symbols whose definition starts with \DOTSB: + '\\DOTSB': '\\dotsb', + '\\coprod': '\\dotsb', + '\\bigvee': '\\dotsb', + '\\bigwedge': '\\dotsb', + '\\biguplus': '\\dotsb', + '\\bigcap': '\\dotsb', + '\\bigcup': '\\dotsb', + '\\prod': '\\dotsb', + '\\sum': '\\dotsb', + '\\bigotimes': '\\dotsb', + '\\bigoplus': '\\dotsb', + '\\bigodot': '\\dotsb', + '\\bigsqcup': '\\dotsb', + '\\implies': '\\dotsb', + '\\impliedby': '\\dotsb', + '\\And': '\\dotsb', + '\\longrightarrow': '\\dotsb', + '\\Longrightarrow': '\\dotsb', + '\\longleftarrow': '\\dotsb', + '\\Longleftarrow': '\\dotsb', + '\\longleftrightarrow': '\\dotsb', + '\\Longleftrightarrow': '\\dotsb', + '\\mapsto': '\\dotsb', + '\\longmapsto': '\\dotsb', + '\\hookrightarrow': '\\dotsb', + '\\iff': '\\dotsb', + '\\doteq': '\\dotsb', + // Symbols whose definition starts with \mathbin: + '\\mathbin': '\\dotsb', + '\\bmod': '\\dotsb', + // Symbols whose definition starts with \mathrel: + '\\mathrel': '\\dotsb', + '\\relbar': '\\dotsb', + '\\Relbar': '\\dotsb', + '\\xrightarrow': '\\dotsb', + '\\xleftarrow': '\\dotsb', + // Symbols whose definition starts with \DOTSI: + '\\DOTSI': '\\dotsi', + '\\int': '\\dotsi', + '\\oint': '\\dotsi', + '\\iint': '\\dotsi', + '\\iiint': '\\dotsi', + '\\iiiint': '\\dotsi', + '\\idotsint': '\\dotsi', + // Symbols whose definition starts with \DOTSX: + '\\DOTSX': '\\dotsx', +}; + +defineMacro("\\dots", function() { + // TODO: If used in text mode, should expand to \textellipsis. + // However, in KaTeX, \textellipsis and \ldots behave the same + // (in text mode), and it's unlikely we'd see any of the math commands + // that affect the behavior of \dots when in text mode. So fine for now + // (until we support \ifmmode ... \else ... \fi). + let thedots = '\\dotso'; + const next = this.expandAfterFuture().text; + if (next in dotsByToken) { + thedots = dotsByToken[next]; + } else if (next.substr(0, 4) === '\\not') { + thedots = '\\dotsb'; + } else if (next in symbols.math) { + if (utils.contains(['bin', 'rel'], symbols.math[next].group)) { + thedots = '\\dotsb'; + } + } + return thedots; +}); + +const spaceAfterDots = { + // \rightdelim@ checks for the following: + ')': true, + ']': true, + '\\rbrack': true, + '\\}': true, + '\\rbrace': true, + '\\rangle': true, + '\\rceil': true, + '\\rfloor': true, + '\\rgroup': true, + '\\rmoustache': true, + '\\right': true, + '\\bigr': true, + '\\biggr': true, + '\\Bigr': true, + '\\Biggr': true, + // \extra@ also tests for the following: + '$': true, + // \extrap@ checks for the following: + ';': true, + '.': true, + ',': true, +}; + +defineMacro("\\dotso", function() { + const next = this.future().text; + if (next in spaceAfterDots) { + return "\\ldots\\,"; + } else { + return "\\ldots"; + } +}); + +defineMacro("\\dotsc", function() { + const next = this.future().text; + // \dotsc uses \extra@ but not \extrap@, instead specially checking for + // ';' and '.', but doesn't check for ','. + if (next in spaceAfterDots && next !== ',') { + return "\\ldots\\,"; + } else { + return "\\ldots"; + } +}); + +defineMacro("\\cdots", function() { + const next = this.future().text; + if (next in spaceAfterDots) { + return "\\@cdots\\,"; + } else { + return "\\@cdots"; + } +}); + +defineMacro("\\dotsb", "\\cdots"); +defineMacro("\\dotsm", "\\cdots"); +defineMacro("\\dotsi", "\\!\\cdots"); +// amsmath doesn't actually define \dotsx, but \dots followed by a macro +// starting with \DOTSX implies \dotso, and then \extra@ detects this case +// and forces the added `\,`. +defineMacro("\\dotsx", "\\ldots\,"); + +// \let\DOTSI\relax +// \let\DOTSB\relax +// \let\DOTSX\relax +defineMacro("\\DOTSI", "\\relax"); +defineMacro("\\DOTSB", "\\relax"); +defineMacro("\\DOTSX", "\\relax"); // http://texdoc.net/texmf-dist/doc/latex/amsmath/amsmath.pdf defineMacro("\\thinspace", "\\,"); // \let\thinspace\, diff --git a/src/symbols.js b/src/symbols.js index 2aeafa70..5adf6600 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -602,7 +602,7 @@ defineSymbol(text, main, inner, "\u2026", "\\textellipsis"); defineSymbol(math, main, inner, "\u2026", "\\mathellipsis"); defineSymbol(text, main, inner, "\u2026", "\\ldots", true); defineSymbol(math, main, inner, "\u2026", "\\ldots", true); -defineSymbol(math, main, inner, "\u22ef", "\\cdots", true); +defineSymbol(math, main, inner, "\u22ef", "\\@cdots", true); defineSymbol(math, main, inner, "\u22f1", "\\ddots", true); defineSymbol(math, main, textord, "\u22ee", "\\vdots", true); defineSymbol(math, main, accent, "\u00b4", "\\acute"); diff --git a/test/katex-spec.js b/test/katex-spec.js index 5f217990..21201442 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -783,6 +783,11 @@ describe("A text parser", function() { it("should parse math within text group", function() { expect(textWithEmbeddedMath).toParse(); }); + + it("should omit spaces after commands", function() { + expect("\\text{\\textellipsis !}") + .toParseLike("\\text{\\textellipsis!}"); + }); }); describe("A color parser", function() { @@ -2314,6 +2319,27 @@ describe("A macro expander", function() { compareParseTree("e^\\foo", "e^1 23", {"\\foo": "123"}); }); + it("should preserve leading spaces inside macro definition", function() { + compareParseTree("\\text{\\foo}", "\\text{ x}", {"\\foo": " x"}); + }); + + it("should preserve leading spaces inside macro argument", function() { + compareParseTree("\\text{\\foo{ x}}", "\\text{ x}", {"\\foo": "#1"}); + }); + + it("should ignore expanded spaces in math mode", function() { + compareParseTree("\\foo", "x", {"\\foo": " x"}); + }); + + it("should consume spaces after macro", function() { + compareParseTree("\\text{\\foo }", "\\text{x}", {"\\foo": "x"}); + }); + + it("should consume spaces between arguments", function() { + compareParseTree("\\text{\\foo 1 2}", "\\text{12end}", {"\\foo": "#1#2end"}); + compareParseTree("\\text{\\foo {1} {2}}", "\\text{12end}", {"\\foo": "#1#2end"}); + }); + it("should allow for multiple expansion", function() { compareParseTree("1\\foo2", "1aa2", { "\\foo": "\\bar\\bar", @@ -2321,6 +2347,41 @@ describe("A macro expander", function() { }); }); + it("should allow for multiple expansion with argument", function() { + compareParseTree("1\\foo2", "12222", { + "\\foo": "\\bar{#1}\\bar{#1}", + "\\bar": "#1#1", + }); + }); + + it("should allow for macro argument", function() { + compareParseTree("\\foo\\bar", "(x)", { + "\\foo": "(#1)", + "\\bar": "x", + }); + }); + + it("should allow for space macro argument (text version)", function() { + compareParseTree("\\text{\\foo\\bar}", "\\text{( )}", { + "\\foo": "(#1)", + "\\bar": " ", + }); + }); + + it("should allow for space macro argument (math version)", function() { + compareParseTree("\\foo\\bar", "()", { + "\\foo": "(#1)", + "\\bar": " ", + }); + }); + + it("should allow for empty macro argument", function() { + compareParseTree("\\foo\\bar", "()", { + "\\foo": "(#1)", + "\\bar": "", + }); + }); + it("should expand the \\overset macro as expected", function() { expect("\\overset?=").toParseLike("\\mathop{=}\\limits^{?}"); expect("\\overset{x=y}{\sqrt{ab}}") diff --git a/test/screenshotter/images/Dots-chrome.png b/test/screenshotter/images/Dots-chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..33e7f2c7b8df0f9193ff7c1aa0462bb926f3a4bd GIT binary patch literal 7858 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 DtmEo+_t zjXnNR|M%vN$L_s>7?U4Q(M z|2fb6;&-!)&wW3+_j%3v@3(Duk7RlAGfYut5b$A8n8Dy+z}S$$#Bh*>p^1}$MVNs@ zjp63D`z43p_D_>*Dq>N%5gZ-2bxr21)b%^peNmaB#IQ53!qd3u&vq|5X{GngN)2ZQqul@hR_4~7oX$Qyzw+M~edgmL^E#AR6fQ?E+y3)9pWZu* ziVt=VCbe_*yKpiIygWGd^6fwNe0ue1KRgf2ZB1Xq!q`!A@Qc{~Z=DC9?w4YF;0+e@ z*z)?qOWl7*zMTrc^|t=#;neg+9E=_%j}s>EPpEiX+MNAf!%DD~!(q{Vshm&Y{OjM^ zl=~JR*!_lGz>`5p){f&qZuL6*pR%*-H5fU*^nP3a@9om(3v3uUzH}b^wtw3fi4X?{ zrn&YU2OcgxzJQ6zV85L0*Q49d?pd`#n2qs9KoyUc0>kImua;RZ{=#Ei^-zJ~>-m%4 z=P{gFuRZ(OmrKTHQjXg-Gt3FE;sdMel4E&r?wfPN_bKz5xi}8|*|mqA@yBg5hUrrw z{9Bir8EQ809#Ho_@5suMP*=N#o$<$8GluOj^=sG}Ew=I=xb6j&U&794v5EJVNw_C11~5XU`D6h0$UgNZgU9%eA9 zkFA?#s}ca2FJsJgVE>ePDLucK z3*LVS{1fuh5v=^?hpE*eXWKWdvuB8xulywfNi^h7 zk!z9qqQT(*{#MPGQ_XSp{x1!Wt+Ho$FU^*a{?c*9!4}3FKil2cM3g^^eLU^X*X-9} za+Mc_A5V+7YTO4NzfC{+yZ@*4r)_`P zyyN~>e4n(PCrsz(=1#X2C2MZ7FSxQ{{|&YW9XIbcx#nzQ=&9?qd2@1|=|Jk{$?X}C~^YaVutjRx+WERN~XZFkG-TYl) zIkIaj5}(iAley)|UE#9DI2Tfe)%bnTDX zw(EbNt)Z+mi~_ax8`txcooln+dT(=r&TkHZ9U2Kq3_}9jQ#Gr z!ffhJ6nK80er}!Y-irUFizHvkWPi(N5|jF7-)6oeX`6XM;A|a++x*$_uF<=u$K0M* z|LP}y$+{bVKfj&3r*M&=mTdMnMh%lU>1~WV3a@QXFq~ar-@xuSvx--?=<5#E>;Dg2 zne_O*?B+)wi>I5PeN}$;@w~lHkIy<>$B^2#w@9L4{kdOaI%&f1Id{A*j(_#%&#JAj zzeyk8Te94?rsgu-i>cy!bzwDwP1)aN z+1sYeCGB~|ykQ^j0oUT4XLZgU|L|e?x_zZ>{Yv|4&Tc*b&+f=(n{Ow5!|%o)*D(Ag z(r{kR=Ig20&(oJ4mstIaVNEV`!Bv?&qwdd!H3`4+`ghfy>B`@imUVQ}{Tqg9C!h1r zjoS2Ot$(P2SDzf~gVn3q_3x%`pC*?w=N9vZN&D z;d14j**BKm*S7bXAjkTE_we<1o=e#!mw#ipV_qZhe)eDAG&}jji+}S?|5NmQZcWLK zML*jm>(5=P?&=p>;?DTv;KR?`o>(tcmt6df;m$p~w$sly%zymrBHQIkySDRvVQ+3% zs;Bo%E2-x$Jb(V>zog4fy&!EjisKqyd;1%?y=9chQ+WNnVDilK$2UCJ_Vl*eU34nL z^Sk)$Z-3^RtbRJEN+)+1B1* zPgG=8Se$+~=hEb!z-k7)9Qi}s&kH6;&gp+>e_L66j@7f(zO&ilbK}gOo$fz8-}v4C z_uQsncfE|2uR6NQIqsfGzJ~1yiLLAj6#*am&p$N-*|x2iFY=%Bd-?C2|1Qt3yE<#D z`W>UZqwo0l=R9rP|KW0S-=rN0i=Vs*W?-|sZn8hJs?9Kq|G@ffj5}6-k-2+r&F?4u z-M;UcK3rU8x&I4K@)fy;$F~_u%BDKDesK zkS{mPD*NW4m$3|W%QE*xK+HVY%&=$67m2%aIkV>L*g8+z$NJ#p)w8cQK0l?MQ+)YP z&`Zf<6uE_2n%$ zJ8%>1z#lQzKgxPX2lFyZ_?LDa$IOUrKKJqRn7`+a!J3yoJ~Oq*)&9U3&aQQQ_q^)jPfjL{^D5 zob}!O>-Ez17c=k9uXub+`}-To`|sFGqE9v++fn!O4qw=bd#{5T-bGwBnSORpPx_70 zUth%PFBM$xTps2ZUw!c1Cgakd+e1t1uT}@X3TDvDnRR{YyoLGy_SD|~-+wzhUNUCo z*Y$78&CV7Y&(WUv^V)I-o1GtbxO<(yV7~tn+k@q|vgPIL_w*hroA>XPB;U{TdtL`K z%-B6YoB6}1Z?}%^vS&_s$a&z=w`b{cE-yPvll!+jD=#u#Ae*C{WL-#%B=RD_IHa_85 ze&|6Yi^73z-sbbFK1ELdA;o&%6-#@rA?J4fu7}^;z4;w2nFQXf)LkES&E)K+>pz+t zbQn*R9`^qBM!9Cs=Cv(C4H4qL^EJNwHk(s>i81(p&Bf-W#}{%idVIZi@L|*Pa{js} zw%N0uNu)pNR$*9ZxA**)>n-JcdiG`Kt~}LEeZG)~@x+$DznAZq`|`}dMLt5+QPijI9F6Mk@Ke^kk$C>Z=*0)GF zu!BM6*V=<>^Pl(s+W?_kQ;E-Z@c!fA~)dTcJIkYZotLezO?dvf<5oD^<< zZ|@V`W>N2dIg12eUEIv@CrP?N+3UQcs2cZy7uQc5p7$ndGK6#lUX06F7AK+Z)Nt;YZtr3%kG_= z?dE;Y_974WyI-kiW3K*)zcx=~L1pes%N2)G7%dXMaQ%+i9`+5fk+=k5A=(UX0J+kU2kj|L2X>+j6upZcEb4*OrO13%w> z%XfERV@X*5RU4^U_AP2ZlYx!?4b7#`7w|D{xc*DDA$%KSMe$3^70z!NE8hI_y`Ce# zKSgXgU&W8$@cUMW+j-;nJ-fwOes{hFgDYG7eYOXSEbpWoWUGBMdG@+p`}+cuLgX4i z4YQ=!&(j}IJM-uYqkM$*)+Z}&A7At1S@yCpou9q3lh0UWGMwVsw~hBe=;nO~5Apu` z>AZZJPVMVp(Vj_*-!asbetmFt*PCnaR5x3c9$;_JuPK+`m+-uQo41-(AB^fEDr>=T;%}<%K{V^*XzyF!@PQQI>{U-6byO=jT-@|bI{GJr=>s_|-R#BIJuChLtL`u~OuzlMrh46<$$MX1k!?7;`1z|n=fDBrf96NaeM6h*bE`bBN8Yi+Je`I2H@K21*5<1S-GRt3xHXLBGr>bD(NvnV|?eYzR%zCC+#-dva7?E`XG z{0+N#;$apqK0NRF{lEIBdiBMX)(rmde_hZHll}B*S>@pws>buJ5^h_z>-=B%z5jR3 z&zJuDv(DZU&YPKXr1LwohS84?3m(YN|H?ZrZii!e{f|^_{y(kr<%*Y_{N(>_QE(*# zyuDVnRr>={gUx3`x34_B!S+Rq;rMpO7j}NZh0-!>QWU=`GUVsave&geEU}mCz{Tdb z^}a6+k6CPqe7Po-dpSSjkD1GUCcTtA_9B#lPmblm(xt~=d`r0eC*`H&q%R5#{_FC> z`fZOUB+j1wY`WKZgJ^$UNtOlG_BS8t<(EqY(e_DE7;taQWr zsq+?JzCG*NcklB8OaC#%1bm&p#&&b&hQIOwRXu*+*X4!X39jnUxXS+E-_^5r(cb(U zGxngh*h1tR=I72b_CCL%VvZTdfokvmi%%adi~L^jQgPE2Ie3e0GtL&i6wD!6eXd^zFvKv(m24zI8bM-}~-m+y4K3r)MJ=D%jxq?$ENx!k-T=DXaVa zXDWz&Sh=E`L8r(%&T8vb`Rb?FqV*%~OB3h&E`7dGk7>h=yX6s^kMH}o`SE|@2G{os zi`&u