# App/Xtn/Mediawiki/Scribunto/Luaj

From XOWA: the free, open-source, offline wiki application

## Source

The luaj_xowa.jar was built using the source at http://sourceforge.net/projects/luaj/files/luaj-3.0/3.0-beta2/luaj-3.0-beta2.zip/download.

Its source is not currently included with XOWA. It is available at the following location: https://sourceforge.net/projects/xowa/files/support/luaj/

## Modification

The luaj_xowa.jar was created for the following reasons:

- Backward compatibility:

  Scribunto is currently using Lua 5.1 whereas luaj 3.0 is designed for Lua 5.2
  Lua 5.2 is not backward-compatible with Lua 5.1; several functions are obsoleted (for example, table.maxn)
  The luaj_xowa.jar was tailored to support Scribunto's 5.1 environment.

- Patches / bug fixes:

  Luaj has a handful of minor issues / defects. They are listed below.

## luaj_xowa changes

### Luaj 2.0.3 errors fixed in 3.0

#### os.time doesn't handle dates before 1970

- fix : incorrect Birth / Date; EX: ru.w:Пушкин,_Александр_Сергеевич
- file: /src/core/org/luaj/vm2/lib/OsLib.java

#### pairs.next fails when setting val to null

- fix : Finnish declension table; EX:d:Latvia
- file: /src/core/org/luaj/vm2/LuaTable.java

### Luaj 2.0.3 features removed from 3.0

#### string.gfind deprecated

- fix : missing Video_game_reviews; EX: w:Sonic_Heroes
- file: /src/core/org/luaj/vm2/lib/StringLib.java
- code: call

          old:
                                    "sub"} );
          new:
                    "sub", "gfind"} );

- code: invoke

          add:
                    case 4:
                    case 9: return StringLib.gmatch( args );

#### math.log10 deprecated

- fix : blank references; EX:w:Earth
- file: /src/jse/org/luaj/vm2/lib/JseMathLib.java
- code:

          math.set("log10", new log10());

          static final class log10 extends UnaryOp { protected double call(double d) { return Math.log10(d); } }

#### math.mod deprecated

- fix : missing table; EX:d:աղբիւր
- file: /src/core/org/luaj/vm2/lib/MathLib.java
- code:

          fmod fmod_func = new fmod();
          math.set("mod", fmod_func);
          math.set("fmod", fmod_func);

#### table.maxn deprecated

file: /src/core/org/luaj/vm2/lib/TableLib.java

- code:

          public LuaValue getn() {
            int len = length();
            for (int n = len; n > 0; --n )
              if ( !rawget(n).isnil() )
                return LuaInteger.valueOf(n);
            return ZERO;
          }

file: /src/core/org/luaj/vm2/lib/TableLib.java

          table.set("maxn", new maxn());

          static class maxn extends OneArgFunction {
            public LuaValue call(LuaValue arg) {
              return LuaValue.valueOf(arg.checktable().maxn());
            }
          }

#### table.getn deprecated

- fix : missing text; EX: d:aceite d:Module:pt-verb-form-of
- file: /src/core/org/luaj/vm2/LuaTable.java

code:

          public LuaValue getn() {
            int len = length();
            for (int n = len; n > 0; --n )
              if ( !rawget(n).isnil() )
                return LuaInteger.valueOf(n);
            return ZERO;
          }

- file: /src/core/org/luaj/vm2/lib/TableLib.java
- code:

          table.set("getn", new getn());

          static class getn extends OneArgFunction {
            public LuaValue call(LuaValue arg) {
              return arg.checktable().getn();
            }
          }

#### automatic arg variable in varargs function deprecated

- fix : Horizontal timeline; EX: w:Cretaceous%E2%80%93Paleogene_extinction_event
- file: /src/core/org/luaj/vm2/LuaClosure.java

code:

          case Lua.OP_GETTABUP: /*  A B C   R(A) := UpValue[B][RK(C)]                       */
            // stack[a] = upValues[i>>>23].getValue().get((c=(i>>14)&0x1ff)>0xff? k[c&0x0ff]: stack[c]);
            // HACK: handle deprecated "arg" for "..."
            int OP_GETTABUP_c = (i>>14)&0x1ff;
            boolean OP_GETTABUP_b = OP_GETTABUP_c>0xff;
            LuaValue OP_GETTABUP_idx = OP_GETTABUP_b ? k[OP_GETTABUP_c&0x0ff]: stack[OP_GETTABUP_c];
            stack[a] = upValues[i>>>23].getValue().get(OP_GETTABUP_idx);
            // HACK: handle deprecated "arg"
            if (    p.is_vararg == 1
              &&    stack[a] == NIL
              &&    OP_GETTABUP_b
              &&  "arg".equals(OP_GETTABUP_idx.tojstring())
              )
              stack[a] = new LuaTable(varargs);
            continue;

### Luaj 3.0 defects

#### os.date does not accept UTC format

- fix : incorrect age in ym; EX:w:Supreme_Court_of_the_United_States
- file: /src/core/org/luaj/vm2/lib/OsLib.java
- proc: invoke.DATE

    boolean utc = false;
    if (s.startsWith("!")) {
      utc = true;
      s = s.substring(1);
    }
    if (s.equals("*t")) {
      Calendar d = Calendar.getInstance();
      long time_in_ms = (long)(t*1000);
      if (utc) {
        java.util.TimeZone current_tz = d.getTimeZone();
        int offset_from_utc = current_tz.getOffset(time_in_ms);
        time_in_ms += -offset_from_utc;
      }

#### string.gsub fails with out_of_bounds error

- fix : blank references; EX:w:Earth
- file: /src/core/org/luaj/vm2/lib/StringLib.java
- proc: gsub

          old:
              if ( anchor )
                break;
          new:
              if ( anchor )
                break;
              if (soffset >= srclen) break; // assert soffset is in bounds, else will throw ArrayIndexOutOfBounds exception;

#### string.gsub fails if string is empty

- fix : blank references; EX:w:Woburn,_Massachusetts
- file: /src/core/org/luaj/vm2/lib/StringLib.java
- proc: gsub

          static Varargs gsub( Varargs args ) {
            LuaString src = args.checkstring( 1 );
            final int srclen = src.length();
            if (srclen == 0) return varargsOf(src, LuaValue.ZERO); // exit early

#### string.format ignores precision for double args

- fix : Convert calls will show full precision for numbers; EX:w:Tomato
- file: /src/core/org/luaj/vm2/lib/StringLib.java

                                                    FormatDesc fdsc = new FormatDesc(args, fmt, i );
                                                    int fdsc_bgn = i;

          old:
                                                    case 'G':
                                                            fdsc.format( result, args.checkdouble( arg ) );
          new:
                                                    case 'G':
                                                            String fmt_str = new String(fmt.m_bytes, fdsc_bgn - 1, fdsc.length + 1);        // -1 to include %; +1 to account for included %; basically get everything between % and f; EX: a%.1fb -> %.1f
                                                            fdsc.format( result, fmt_str, args.checkdouble( arg ));

- proc: format

          old:
                            buf.append( v) );
          new:
                            // buf.append( String.valueOf( x ) );
                            if (fmt.startsWith("%0."))
                                    fmt = "%" + fmt.substring(2);                         // remove leading 0, else MissingFormatWidthException
                            int fmt_len = fmt.length();
                            if (fmt_len > 1 && fmt.charAt(fmt_len - 2) == '.')   // penultimmate char has "."
                                    fmt = fmt.substring(0, fmt_len - 1) + "0" + fmt.charAt(fmt_len - 1);  // add trailing 0, else UnknownFormatConversionException; EX: "02.f" -> "02.0f"
                            buf.append( String.format(fmt, v) );    // call String.format

- note: also fixes format failures
  - %0.1f -> remove leading 0
  - %02.f -> add trailing 0 after .
- note: forces a 1.5 JRE (as opposed to 1.3)

#### string.gmatch issues

- fix : Multiple pages in enwiki's Wikipedia namespace; EX: Wikipedia:CS1/test_basics
- file: /src/core/org/luaj/vm2/lib/StringLib.java
- proc: GmatchAux.invoke

          old:
                            for ( ; soffset<srclen; soffset++ ) {
          new:
                            for ( ; soffset<=srclen; soffset++ ) {

          old:
                                            soffset = res;
          new:
                                            int soffset_adj = res == soffset ? 1 : 0;
                                            soffset = res + soffset_adj;

#### string.tonumber should trim all whitespace

- fix: Population tables; EX: w:Woburn,_Massachusetts
- file: /src/core/org/luaj/vm2/LuaString.java
- proc: scannumber

            // trim ws
            int idx = i;
            while (idx < j) {
                    switch (m_bytes[idx]) {
                            case 9: case 10: case 13: case 32:
                                    ++idx;
                                    i = idx;
                                    break;
                            default:
                                    idx = j;
                                    break;
                    }
            }
            idx = j - 1;
            while (idx >= i) {
                    switch (m_bytes[idx]) {
                            case 9: case 10: case 13: case 32:
                                    j = idx;
                                    --idx;
                                    break;
                            default:
                                    idx = i -1;
                                    break;
                    }
            }

#### multi-byte strings not fully supported

- fix : Thai calendar; EX:th.w:เหตุการณ์ปัจจุบัน
- file: /src/core/org/luaj/vm2/LuaString.java

            public static LuaString valueOf(char[] chars, int off, int len) {
                    // COMMENTED: does not handle 2+ byte chars; assumes 1 char = 1 byte
    //              byte[] b = new byte[len];
    //              for ( int i=0; i<len; i++ )
    //                      b[i] = (byte) chars[i + off];
    //              return valueOf(b, 0, len);
                    int bry_len = 0;
                    for (int i = 0; i < len; i++) {      // iterate over chars to sum all single / multi-byte chars
                            int b_len = LuaString.Utf16_Len_by_char((int)(chars[i + off]));
                            if (b_len == 4) ++i;            // 4 bytes; surrogate pair; skip next char;
                            bry_len += b_len;
                    }
                    byte[] bry = new byte[bry_len];
                    int bry_idx = 0;
                int i = 0;
                while (i < len) {
                  char c = chars[i + off];
                  int b_len = Utf16_Encode_char(c, chars, i, bry, bry_idx);
                  bry_idx += b_len;
                  i += b_len == 4 ? 2 : 1;          // 4 bytes; surrogate pair; skip next char;
                }
                    return valueOf(bry, 0, bry_len);
            }

            public static String decodeAsUtf8(byte[] bytes, int offset, int length) {
                    // COMMENTED: does not handle 3+ byte chars
    //              int i,j,n,b;
    //              for ( i=offset,j=offset+length,n=0; i<j; ++n ) {
    //                      switch ( 0xE0 & bytes[i++] ) {
    //                      case 0xE0: ++i;
    //                      case 0xC0: ++i;
    //                      }
    //              }
    //              char[] chars=new char[n];
    //              for ( i=offset,j=offset+length,n=0; i<j; ) {
    //                      chars[n++] = (char) (
    //                              ((b=bytes[i++])>=0||i>=j)? b:
    //                              (b<-32||i+1>=j)? (((b&0x3f) << 6) | (bytes[i++]&0x3f)):
    //                                      (((b&0xf) << 12) | ((bytes[i++]&0x3f)<<6) | (bytes[i++]&0x3f)));
    //              }
    //              return new String(chars);
                    return new String(bytes, offset, length, java.nio.charset.Charset.forName("UTF-8"));
            }

            public static int lengthAsUtf8(char[] chars) {
                    // COMMENTED: does not handle 3+ byte chars
    //              int i,b;
    //              char c;
    //              for ( i=b=chars.length; --i>=0; )
    //                      if ( (c=chars[i]) >=0x80 )
    //                              b += (c>=0x800)? 2: 1;
    //              return b;
                    int len = chars.length;
                    int rv = 0;
                    for (int i = 0; i < len; i++) {
                            int b_len = LuaString.Utf16_Len_by_char(chars[i]);
                            if (b_len == 4) ++i;            // 4 bytes; surrogate pair; skip next char;
                            rv += b_len;
                    }
                    return rv;
            }

            public static int encodeToUtf8(char[] chars, int nchars, byte[] bytes, int off) {
                    // COMMENTED: does not handle 4+ byte chars; already using Encode_by_int, so might as well be consistent
    //              char c;
    //              int j = off;
    //              for ( int i=0; i<nchars; i++ ) {
    //                      if ( (c = chars[i]) < 0x80 ) {
    //                              bytes[j++] = (byte) c;
    //                      } else if ( c < 0x800 ) {
    //                              bytes[j++] = (byte) (0xC0 | ((c>>6)  & 0x1f));
    //                              bytes[j++] = (byte) (0x80 | ( c      & 0x3f));
    //                      } else {
    //                              bytes[j++] = (byte) (0xE0 | ((c>>12) & 0x0f));
    //                              bytes[j++] = (byte) (0x80 | ((c>>6)  & 0x3f));
    //                              bytes[j++] = (byte) (0x80 | ( c      & 0x3f));
    //                      }
    //              }
    //              return j - off;
                    int bry_idx = off;
                int i = 0;
                while (i < nchars) {
                  char c = chars[i];
                  int bytes_read = Utf16_Encode_char(c, chars, i, bytes, bry_idx);
                  bry_idx += bytes_read;
                  i += bytes_read == 4 ? 2 : 1;     // 4 bytes; surrogate pair; skip next char;
                }
                    return nchars;  // NOTE: code returned # of bytes which is wrong; Globals.UTF8Stream.read caches rv as j which is used as index to char[] not byte[]; will throw out of bounds exception if bytes returned
            }

            private static int Utf16_Len_by_char(int c) {
                    if         ((c >       -1)
                             && (c <      128))  return 1;               // 1 <<  7
                    else if (c <     2048)       return 2;               // 1 << 11
                    else if((c > 55295)                                          // 0xD800
                             && (c < 56320))
                            return 4;               // 0xDFFF
                    else if (c <   65536)        return 3;               // 1 << 16
                    else throw new RuntimeException("UTF-16 int must be between 0 and 2097152; char=" + c);
            }
            public static int Utf16_Len_by_int(int c) {
                    if         ((c >       -1)
                             && (c <      128))  return 1;               // 1 <<  7
                    else if (c <     2048)       return 2;               // 1 << 11
                    else if (c <   65536)        return 3;               // 1 << 16
                    else if (c < 2097152)        return 4;
                    else throw new RuntimeException("UTF-16 int must be between 0 and 2097152; char=" + c);
            }
            public static int Utf8_Len_of_char_by_1st_byte(byte b) {// SEE:w:UTF-8
                    int i = b & 0xff;   // PATCH.JAVA:need to convert to unsigned byte
                    switch (i) {
                            case   0: case   1: case   2: case   3: case   4: case   5: case   6: case   7: case   8: case   9: case  10: case  11: case  12: case  13: case  14: case  15:
                            case  16: case  17: case  18: case  19: case  20: case  21: case  22: case  23: case  24: case  25: case  26: case  27: case  28: case  29: case  30: case  31:
                            case  32: case  33: case  34: case  35: case  36: case  37: case  38: case  39: case  40: case  41: case  42: case  43: case  44: case  45: case  46: case  47:
                            case  48: case  49: case  50: case  51: case  52: case  53: case  54: case  55: case  56: case  57: case  58: case  59: case  60: case  61: case  62: case  63:
                            case  64: case  65: case  66: case  67: case  68: case  69: case  70: case  71: case  72: case  73: case  74: case  75: case  76: case  77: case  78: case  79:
                            case  80: case  81: case  82: case  83: case  84: case  85: case  86: case  87: case  88: case  89: case  90: case  91: case  92: case  93: case  94: case  95:
                            case  96: case  97: case  98: case  99: case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: case 108: case 109: case 110: case 111:
                            case 112: case 113: case 114: case 115: case 116: case 117: case 118: case 119: case 120: case 121: case 122: case 123: case 124: case 125: case 126: case 127:
                            case 128: case 129: case 130: case 131: case 132: case 133: case 134: case 135: case 136: case 137: case 138: case 139: case 140: case 141: case 142: case 143:
                            case 144: case 145: case 146: case 147: case 148: case 149: case 150: case 151: case 152: case 153: case 154: case 155: case 156: case 157: case 158: case 159:
                            case 160: case 161: case 162: case 163: case 164: case 165: case 166: case 167: case 168: case 169: case 170: case 171: case 172: case 173: case 174: case 175:
                            case 176: case 177: case 178: case 179: case 180: case 181: case 182: case 183: case 184: case 185: case 186: case 187: case 188: case 189: case 190: case 191:
                                    return 1;
                            case 192: case 193: case 194: case 195: case 196: case 197: case 198: case 199: case 200: case 201: case 202: case 203: case 204: case 205: case 206: case 207:
                            case 208: case 209: case 210: case 211: case 212: case 213: case 214: case 215: case 216: case 217: case 218: case 219: case 220: case 221: case 222: case 223:
                                    return 2;
                            case 224: case 225: case 226: case 227: case 228: case 229: case 230: case 231: case 232: case 233: case 234: case 235: case 236: case 237: case 238: case 239:
                                    return 3;
                            case 240: case 241: case 242: case 243: case 244: case 245: case 246: case 247:
                                    return 4;
                            default: throw new RuntimeException("invalid initial utf8 byte; byte=" + b);
                    }
            }
            public static int Utf16_Decode_to_int(byte[] ary, int pos) {
                    byte b0 = ary[pos];
                    if              ((b0 & 0x80) == 0) {
                            return  b0;
                    }
                    else if ((b0 & 0xE0) == 0xC0) {
                            return  ( b0           & 0x1f) <<  6
                                    |       ( ary[pos + 1] & 0x3f)
                                    ;
                    }
                    else if ((b0 & 0xF0) == 0xE0) {
                            return  ( b0           & 0x0f) << 12
                                    |       ((ary[pos + 1] & 0x3f) <<  6)
                                    |       ( ary[pos + 2] & 0x3f)
                                    ;
                    }
                    else if ((b0 & 0xF8) == 0xF0) {
                            return  ( b0           & 0x07) << 18
                                    |       ((ary[pos + 1] & 0x3f) << 12)
                                    |       ((ary[pos + 2] & 0x3f) <<  6)
                                    |       ( ary[pos + 3] & 0x3f)
                                    ;
                    }
                    else throw new RuntimeException("invalid utf8 byte: byte=" + b0);
            }
            public static int Utf16_Encode_int(int c, byte[] src, int pos) {
                    if         ((c > -1)
                             && (c < 128)) {
                            src[  pos]      = (byte)c;
                            return 1;
                    }
                    else if (c < 2048) {
                            src[  pos]      = (byte)(0xC0 | (c >>   6));
                            src[++pos]      = (byte)(0x80 | (c & 0x3F));
                            return 2;
                    }
                    else if (c < 65536) {
                            src[pos]        = (byte)(0xE0 | (c >> 12));
                            src[++pos]      = (byte)(0x80 | (c >>  6) & 0x3F);
                            src[++pos]      = (byte)(0x80 | (c        & 0x3F));
                            return 3;
                    }
                    else if (c < 2097152) {
                            src[pos]        = (byte)(0xF0 | (c >> 18));
                            src[++pos]      = (byte)(0x80 | (c >> 12) & 0x3F);
                            src[++pos]      = (byte)(0x80 | (c >>  6) & 0x3F);
                            src[++pos]      = (byte)(0x80 | (c        & 0x3F));
                            return 4;
                    }
                    else throw new RuntimeException("UTF-16 int must be between 0 and 2097152; char=" + c);
            }
            public static int Utf16_Encode_char(int c, char[] c_ary, int c_pos, byte[] b_ary, int b_pos) {
                    if         ((c >   -1)
                             && (c < 128)) {
                            b_ary[  b_pos]  = (byte)c;
                            return 1;
                    }
                    else if (c < 2048) {
                            b_ary[  b_pos]  = (byte)(0xC0 | (c >>   6));
                            b_ary[++b_pos]  = (byte)(0x80 | (c & 0x3F));
                            return 2;
                    }
                    else if((c > 55295)                          // 0xD800
                             && (c < 56320)) {                   // 0xDFFF
                            if (c_pos >= c_ary.length)
                                    throw new RuntimeException("incomplete surrogate pair at end of string; char=" + c);
                            int nxt_char = c_ary[c_pos + 1];
                            int v = Utf16_Surrogate_merge(c, nxt_char);
                            b_ary[b_pos]    = (byte)(0xF0 | (v >> 18));
                            b_ary[++b_pos]  = (byte)(0x80 | (v >> 12) & 0x3F);
                            b_ary[++b_pos]  = (byte)(0x80 | (v >>  6) & 0x3F);
                            b_ary[++b_pos]  = (byte)(0x80 | (v        & 0x3F));
                            return 4;
                    }
                    else {
                            b_ary[b_pos]    = (byte)(0xE0 | (c >> 12));
                            b_ary[++b_pos]  = (byte)(0x80 | (c >>  6) & 0x3F);
                            b_ary[++b_pos]  = (byte)(0x80 | (c        & 0x3F));
                            return 3;
                    }
            }
            private static int Utf16_Surrogate_merge(int hi, int lo) { // REF: http://perldoc.perl.org/Encode/Unicode.html
                    return 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00);
            }

- file: /src/core/org/luaj/vm2/compiler/LexState.java
- proc: read_string

                                                    if (c > UCHAR_MAX)
                                                            lexerror("escape sequence too large", TK_STRING);
                                                    save(c, false); // NOTE: specify that c is integer and does not need conversion; EX: \128 -> 128 -> (char)128, not Utf8_encode(128)

- file: /src/core/org/luaj/vm2/compiler/LexState.java

            void save(int c) {save(c, true);}
            void save(int c, boolean c_might_be_utf8) {
                    int bytes_len = c_might_be_utf8 ? LuaString.Utf8_Len_of_char_by_1st_byte((byte)c) : 1;
                    if (bytes_len > 1) { // c is 1st byte of utf8 multi-byte sequence; read required number of bytes and convert to char; EX: left-arrow is serialized in z as 226,134,144; c is currently 226; read 134 and 144 and convert to left-arrow
                            temp_bry[0] = (byte)c;
                            for (int i = 1; i < bytes_len; i++) {
                                    nextChar();
                                    temp_bry[i] = (byte)current;
                            }
                            c = LuaString.Utf16_Decode_to_int(temp_bry, 0);
                    }
                    if ( buff == null || nbuff + 1 > buff.length )
                            buff = LuaC.realloc( buff, nbuff*2+1 );
                    buff[nbuff++] = (char)c;
            }
            private static byte[] temp_bry = new byte[6];

### build.xml

- note: this change is needed to get luaj to compile with the String.format(String, double) call
- file: build.xml

          old:
            <javac destdir="build/jme/classes" encoding="utf-8" source="1.3" target="1.2" bootclasspathref="wtk-libs"
              srcdir="build/jme/src"/>
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.3" target="1.3"
              classpath="lib/bcel-5.2.jar"
              srcdir="build/jse/src"
              excludes="**/script/*,**/Lua2Java*,lua*"/>
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.5" target="1.5"
              classpath="build/jse/classes"
              srcdir="build/jse/src"
              includes="**/script/*,**/Lua2Java*"/>
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.3" target="1.3"
              classpath="build/jse/classes"
              srcdir="build/jse/src"
              includes="lua*"/>
          new:
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.5" target="1.5"
              classpath="lib/bcel-5.2.jar"
              srcdir="build/jse/src"
              excludes="**/script/*,**/Lua2Java*,lua*"/>
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.5" target="1.5"
              classpath="build/jse/classes"
              srcdir="build/jse/src"
              includes="**/script/*,**/Lua2Java*"/>
            <javac destdir="build/jse/classes" encoding="utf-8" source="1.5" target="1.5"
              classpath="build/jse/classes"
              srcdir="build/jse/src"
              includes="lua*"/>

### Luaj tests

    package org.luaj.vm2;
    import org.luaj.vm2.lib.StringLib;
    import junit.framework.*;
    public class Xowa_tst extends TestCase {
            private Xowa_fxt fxt = new Xowa_fxt();
            public void test_tonumber_ws() {
                    fxt.Test_tonumber_int("123"                                           , 123);
                    fxt.Test_tonumber_int("\t\n\r 123\t\n\r"              , 123);
                    fxt.Test_tonumber_nil("1a");
                    fxt.Test_tonumber_nil("1 2");
                    fxt.Test_tonumber_nil("");
                    fxt.Test_tonumber_nil("\t\n\r \t\n\r");
            }
            public void test_gsub() {
                    fxt.Test_gsub("abc", "a", "A", "Abc");  // basic
                    fxt.Test_gsub("a#b", "#", "", "ab");    // match() fails when shortening string
                    fxt.Test_gsub("", "%b<>", "A", "");               // balance() fails with out of index when find is blank
            }
            public void test_format() {
                    fxt.Test_format("%.1f"        , "1.23", "1.2");   // apply precision; 1 decimal place
                    fxt.Test_format("(%.1f)", "1.23", "(1.2)");       // handle substring; format_string should be "%.1f" not "(%.1f)"
                    fxt.Test_format("%0.1f"       , "1.23", "1.2");   // handle invalid padding of 0
                    fxt.Test_format("%02.f"       , "1.23", "01");    // handle missing precision
            }
    }
    class Xowa_fxt {
            public void Test_tonumber_int(String raw, int expd) {
                    LuaString actl_str = LuaString.valueOf(raw);
                    LuaInteger actl_int = (LuaInteger)actl_str.tonumber();
                    Assert.assertEquals(expd, actl_int.v);
            }
            public void Test_tonumber_nil(String raw) {
                    LuaString actl_str = LuaString.valueOf(raw);
                    Assert.assertEquals(LuaValue.NIL, actl_str.tonumber());
            }
            public void Test_gsub(String text, String regx, String repl, String expd) {
                    Varargs actl_args = StringLib.gsub_test(LuaValue.varargsOf(new LuaValue[] {LuaValue.valueOf(text), LuaValue.valueOf(regx), LuaValue.valueOf(repl)}));
                    Assert.assertEquals(expd, actl_args.checkstring(1).tojstring());
            }
            public void Test_format(String fmt, String val, String expd) {
                    Varargs actl_args = StringLib.format_test(LuaValue.varargsOf(new LuaValue[] {LuaValue.valueOf(fmt), LuaValue.valueOf(val)}));
                    Assert.assertEquals(expd, actl_args.checkstring(1).tojstring());
            }
    }

## Scribunto related

None of these changes affect the luaj_xowa.jar. They are noted for comprehensiveness's sake.

Note that the $engines variable refers to /xowa/bin/any/lua/mediawiki/extensions/Scribunto/engines/

### getfenv/setfenv deprecated

- add lua-compat-env

  source: https://github.com/davidm/lua-compat-env
  target: $engines/LuaCommon/lualib/lua-compat-env .lua

- add alias to $engines/Luaj/mw_main.lua

  _G.getfenv = require 'compat_env'.getfenv
  _G.setfenv = require 'compat_env'.setfenv

- change xowa.jar to load debugLibrary

  Globals.load(new DebugLib());

### loadString deprecated

- add alias to $engines/Luaj/mw_main.lua

  _G.loadstring = load

### table.unpack deprecated

- add alias to $engines/Luaj/mw_main.lua

  _G.unpack = table.unpack

## Miscellaneous changes

- 2015-10-11: Increased LUAI_MAXVALUES from 200 to 249 else <ref> fails because function citation0 in en.wikipedia.org/wiki/Module:Citation/CS1 uses more than 200 local variables
  - See: http://www.lua.org/source/5.1/luaconf.h.html LUAI_MAXVARS is the maximum number of local variables per function (must be smaller than 250).

### XOWA

- Main page
- Screenshots
- Video
- Download XOWA
- Download wikis

### Getting started

- Requirements
- Installation
- Simple Wikipedia
- English Wikipedia
- Other Wikipedias

### Android

- Setup
- Video

### Help

- About
- Contents
- Media
- Feedback

### Blog

- Current

### Links

- Wikimedia dumps
- XOWA @ archive.org
- English Wikipedia

### Donate

- archive.org
- Wikipedia