diff --git a/100_core/src/gplx/Int_.java b/100_core/src/gplx/Int_.java index d2380050b..d6ea40a62 100644 --- a/100_core/src/gplx/Int_.java +++ b/100_core/src/gplx/Int_.java @@ -28,6 +28,7 @@ public class Int_ { , Null = Int_.Min_value , Base1 = 1 // for super 1 lists / arrays; EX: PHP; [a, b, c]; [1] => a , Offset_1 = 1 // common symbol for + 1 after current pos; EX: String_.Mid(lhs + Offset_1, rhs) + , Zero = 0 ; public static int Cast(Object obj) { diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java index 5b42241bf..e5ace16c5 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java @@ -33,6 +33,7 @@ public class XophpArray implements Bry_bfr_able { int len = hash.Len(); return len == 0 ? null : ((XophpArrayItm)hash.Get_at(len - 1)).Val(); } + public boolean Eq_to_new() {return hash.Count() == 0;}// same as "array === []" public void unset(int key) {unset(Int_.To_str(key));} public void unset(String key) { hash.Del(key); @@ -103,9 +104,17 @@ public class XophpArray implements Bry_bfr_able { } return this; } + public void Concat_str(int i, String s) { + this.Set(i, this.Get_at_str(i) + s); + } + public XophpArray Get_at_ary_or_null(int i) { + Object rv = Get_at(i); + return Type_.Eq_by_obj(rv, XophpArray.class) ? (XophpArray)rv : null; + } public XophpArray Get_at_ary(int i) {return (XophpArray)Get_at(i);} - public String Get_at_str(int i) {return (String)Get_at(i);} + public boolean Get_at_bool(int i) {return Bool_.Cast(Get_at(i));} public int Get_at_int(int i) {return Int_.Cast(Get_at(i));} + public String Get_at_str(int i) {return (String)Get_at(i);} public Object Get_at(int i) { if (i < 0 || i >= hash.Len()) return null; XophpArrayItm itm = (XophpArrayItm)hash.Get_at(i); @@ -123,10 +132,15 @@ public class XophpArray implements Bry_bfr_able { } public Object Get_by_obj(Object key) {return Get_by(Object_.Xto_str_strict_or_null(key));} public Object Get_by(int key) {return Get_by(Int_.To_str(key));} + public boolean Get_by_bool_or(String key, boolean or) {Object rv = this.Get_by(key); return rv == null ? or : Bool_.Cast(rv);} public boolean Get_by_bool(String key) {return Bool_.Cast(this.Get_by(key));} + public int Get_by_int_or(String key, int or) {Object rv = this.Get_by(key); return rv == null ? or : Int_.Cast(rv);} public int Get_by_int(String key) {return Int_.Cast(this.Get_by(key));} + public XophpArray Get_by_ary_or(String key, XophpArray or) {Object rv = this.Get_by(key); return rv == null ? or : (XophpArray)rv;} public XophpArray Get_by_ary(String key) {return (XophpArray)this.Get_by(key);} public String Get_by_str(char key) {return (String)this.Get_by(Char_.To_str(key));} + public String Get_by_str(int key) {return (String)this.Get_by(Int_.To_str(key));} + public String Get_by_str_or(String key, String or) {Object rv = this.Get_by(key); return rv == null ? or : (String)rv;} public String Get_by_str(String key) {return (String)this.Get_by(key);} public Object Get_by(String key) { XophpArrayItm itm = (XophpArrayItm)hash.Get_by(key); @@ -180,6 +194,35 @@ public class XophpArray implements Bry_bfr_able { } return rv; } + @Override public boolean equals(Object obj) { + if (obj == null) return false; + if (!Type_.Eq_by_obj(obj, XophpArray.class)) return false; + XophpArray comp = (XophpArray)obj; + + // compare lens + int this_len = this.Len(); + int comp_len = comp.Len(); + if (this_len != comp_len) return false; + + // loop items + for (int i = 0; i < this_len; i++) { + XophpArrayItm this_itm = this.Get_at_itm(i); + XophpArrayItm comp_itm = comp.Get_at_itm(i); + if (!Object_.Eq(this_itm, comp_itm)) + return false; + } + return true; + } + @Override public int hashCode() { + int rv = 0; + int len = this.Len(); + for (int i = 0; i < len; i++) { + XophpArrayItm itm = this.Get_at_itm(i); + rv = (31 * rv) + itm.hashCode(); + } + return rv; + } + public static XophpArray New(Object... vals) { XophpArray rv = new XophpArray(); for (Object val : vals) diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayItm.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayItm.java index 9ee058afe..1d2afa237 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayItm.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayItm.java @@ -39,6 +39,36 @@ public class XophpArrayItm implements Bry_bfr_able { bfr.Add_obj(val).Add_byte_space(); } } + @Override public boolean equals(Object obj) { + if (obj == null) return false; + XophpArrayItm comp = (XophpArrayItm)obj; + + // compare key + if (key_is_int) { + if (this.key_as_int != comp.key_as_int) + return false; + } + else { + if (!String_.Eq(this.key, comp.key)) + return false; + } + + // compare val + if (this.val == null && comp.val != null) + return false; + else if (this.val != null && comp.val == null) + return false; + else if (this.val == null && comp.val == null) + return true; + else + return Object_.Eq(this.val, comp.val); + } + @Override public int hashCode() { + int rv = 0; + rv = (31 * rv) + (key_is_int ? key_as_int : key.hashCode()); + rv = (31 * rv) + val.hashCode(); + return rv; + } public static XophpArrayItm New_int(int key, Object val) {return new XophpArrayItm(Bool_.Y, key, Int_.To_str(key), val);} public static XophpArrayItm New_str(String key, Object val) {return new XophpArrayItm(Bool_.N, -1, key , val);} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java index 4d00dee05..495c01680 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java @@ -152,6 +152,11 @@ public class XophpArray_ { public static boolean array_is_empty(Ordered_hash array) { return array.Len() == 0; } + + public static boolean array_key_exists(String key, XophpArray array) {return array.Has(key);} + public static boolean array_key_exists(int key, XophpArray array) {return array.Has(Int_.To_str(key));} + + public static void unset(XophpArray array, int i) {array.unset(i);} public static void unset(Ordered_hash array, Object key) { array.Del(key); } @@ -164,12 +169,6 @@ public class XophpArray_ { rv[i - 1] = ary[i]; return rv; } - public static boolean in_array(String needle, String[] haystack) { - for (String hay : haystack) - if (String_.Eq(hay, needle)) - return true; - return false; - } // REF.PHP:https://www.php.net/manual/en/function.array-map.php public static XophpArray array_map(XophpCallbackOwner callback_owner, String method, XophpArray array) { @@ -204,7 +203,47 @@ public class XophpArray_ { return sb.To_str_and_clear(); } - public static int count(XophpArray array) { - return array.count(); + public static int count(XophpArray array) {return array.count();} + public static boolean count_bool(XophpArray array) {return array.count_bool();} + public static Object array_pop(XophpArray array) {return array.pop();} + public static boolean isset(XophpArray array, int key) {return XophpObject_.isset_obj(array.Get_at(key));} + public static boolean isset(XophpArray array, String key) {return XophpObject_.isset_obj(array.Get_by(key));} + public static boolean is_array(XophpArray array) {return array != null;} + + // REF.PHP: https://www.php.net/manual/en/function.in-array.php + public static boolean in_array(Object needle, XophpArray haystack) {return in_array(needle, haystack, false);} + public static boolean in_array(Object needle, XophpArray haystack, boolean strict) { + // if strict, cache needleType + Class needleType = null; + if (strict && needle != null) { + needleType = Type_.Type_by_obj(needle); + } + + // loop haystack to find match + int haystack_len = haystack.Len(); + for (int i = 0; i < haystack_len; i++) { + Object val = haystack.Get_at(i); + + // if strict, compare types + if (strict) { + if (needle != null && val == null) { + return false; + } + else if (needle == null && val != null) { + return false; + } + else if (needle != null && val != null) { + if (!Type_.Eq_by_obj(val, needleType)) { + return false; + } + } + } + + // compare objects + if (Object_.Eq(needle, val)) { + return true; + } + } + return false; } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java index 631bb8e84..b3c9c221a 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java @@ -257,6 +257,25 @@ public class XophpArray__tst { // REF:https://www.php.net/manual/en/function.arr , XophpArray_.implode(" ", orig) ); } + @Test public void in_array() { + // PHP samples + XophpArray array; + // Example #1 + array = XophpArray.New("Mac", "NT", "Irix", "Linux"); + Gftest.Eq__bool(Bool_.Y, XophpArray_.in_array("Irix", array)); + Gftest.Eq__bool(Bool_.N, XophpArray_.in_array("mac" , array)); + + // Example #2 + array = XophpArray.New(12.4d, 1.13d); + Gftest.Eq__bool(Bool_.N, XophpArray_.in_array("12.4", array, true)); + Gftest.Eq__bool(Bool_.Y, XophpArray_.in_array( 1.13d, array, true)); + + // Example #3 + array = XophpArray.New(XophpArray.New('p', 'h'), XophpArray.New('p', 'r'), 'o'); + Gftest.Eq__bool(Bool_.Y, XophpArray_.in_array(XophpArray.New('p', 'h'), array)); + Gftest.Eq__bool(Bool_.N, XophpArray_.in_array(XophpArray.New('f', 'i'), array)); + Gftest.Eq__bool(Bool_.Y, XophpArray_.in_array('o', array)); + } } class XophpArray__fxt { public XophpArray Make() {return new XophpArray();} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpBool_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpBool_.java index 1f51dd3ca..de98c0490 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpBool_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpBool_.java @@ -15,6 +15,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; public class XophpBool_ { + public static final boolean Null = false; public static boolean is_true(byte[] val) {return val != null && is_true(String_.new_u8(val));} public static boolean is_true(String val) { // REF: https://www.php.net/manual/en/language.types.boolean.php#language.types.boolean.casting diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpDouble_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpDouble_.java new file mode 100644 index 000000000..83206e743 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpDouble_.java @@ -0,0 +1,20 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpDouble_ { + public static int intval(double val) {return (int)val;} +} + diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java index d983d02a8..dce2666eb 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java @@ -16,7 +16,8 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; public class XophpInt_ { public static final int False = 0; // REF.PHP:https://www.php.net/manual/en/language.types.boolean.php - public static boolean is_true(int val) {return val != -1;} // handles code like "if ($var === false)" where var is an Object; + public static boolean is_true(int val) {return val != 0;} // handles code like "if ($var)" where var is an Object; + public static boolean is_false(int val) {return val < 0;} // handles XophpInt_.False as well as strpos.notFound (-1) public static String strval(int number) { return Int_.To_str(number); } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpObject_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpObject_.java index a1f6627f4..ba7e83cd9 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpObject_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpObject_.java @@ -17,6 +17,7 @@ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; public class XophpObject_ { public static final Object False = null; // handles code like "if ($var === false)" where var is an Object; public static boolean is_true(Object val) {return val != null;} + public static boolean is_false(Object val) {return val == null;} public static boolean is_null(Object val) {return val == null;} // REF.PHP:http://php.net/manual/en/function.empty.php diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java index 96fd05966..2b5a3fc7e 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java @@ -17,8 +17,25 @@ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; import gplx.langs.regxs.*; import gplx.core.strings.*; import gplx.core.primitives.*; import gplx.core.bits.*; public class XophpRegex_ { + // REF.PHP: https://www.php.net/manual/en/function.preg-quote.php + // The special regular expression characters are: + private static final Hash_adp preg_quote_hash = Hash_adp_.New().Add_many_as_key_and_val + ('.', '\\', '+', '*', '?', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '-', '#'); + public static String preg_quote(String str, String delimiter) {// NOTE: "String delimiter" not used b/c Java / XO does not allow symbolic quotes; EX: "/abc/i" + String_bldr sb = String_bldr_.new_(); + int len = String_.Len(str); + for (int i = 0; i < len; i++) { + char c = String_.CharAt(str, i); + if (preg_quote_hash.Has(c)) { + sb.Add("\\"); + } + sb.Add(c); + } + return sb.To_str_and_clear(); + } public static boolean preg_match_bool(Regx_adp pattern, int modifier, String subject) {return preg_match_bool(pattern, modifier, subject, null, 0, 0);} public static boolean preg_match_bool(Regx_adp pattern, String subject, XophpArray matches, int flags, int offset) {return preg_match(pattern, MODIFIER_NONE, subject, matches, flags, offset) == FOUND;} + public static boolean preg_match_bool(String pattern, int modifier, String subject, XophpArray matches, int flags, int offset) {return preg_match(Regx_adp_.new_(pattern), modifier, subject, matches, flags, offset) == FOUND;} public static boolean preg_match_bool(Regx_adp pattern, int modifier, String subject, XophpArray matches, int flags, int offset) {return preg_match(pattern, modifier, subject, matches, flags, offset) == FOUND;} public static int preg_match(Regx_adp pattern, String subject) {return preg_match(pattern, MODIFIER_NONE, subject, null, 0, 0);} public static int preg_match(Regx_adp pattern, int modifier, String subject) {return preg_match(pattern, modifier, subject, null, 0, 0);} @@ -61,8 +78,8 @@ public class XophpRegex_ { private static void preg_match_fill(String subject, XophpArray matches, int flags, Regx_match match, String match_str, Regx_group[] grps, int grps_len) { for (int i = 0; i < grps_len; i++) { Regx_group grp = grps[i]; - if (!grp.Rslt()) continue; // ignore non matches in group; EX: "1" and "^-?(([0-9]+)(?:\\.([0-9]+))?)" returns a match=false for group(2) - String grp_match = grp.Val(); + // TOMBSTONE: if (!grp.Rslt()) continue; // ignore non matches in group; EX: "1" and "^-?(([0-9]+)(?:\\.([0-9]+))?)" returns a match=false for group(2) + String grp_match = grp.Rslt() ? grp.Val() : ""; if (flags == PREG_OFFSET_CAPTURE) { matches.Add(XophpArray.New(grp_match, grp.Bgn())); } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java index 7fbf58d13..54b6c1a59 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java @@ -51,6 +51,10 @@ public class XophpRegex__tst { .Add("baz") ); } + @Test public void preg_quote() { + Gftest.Eq__str("abc", XophpRegex_.preg_quote("abc", "/")); + Gftest.Eq__str("\\.\\\\\\+\\*\\?\\[\\^\\]\\$\\(\\)\\{\\}\\=\\!\\<\\>\\|\\:\\-\\#", XophpRegex_.preg_quote(".\\+*?[^]$(){}=!<>|:-#", "/")); + } } class XophpRegex__fxt { private String_bldr print_php = null;//String_bldr_.new_(); diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpStringRef.java b/400_xowa/src/gplx/xowa/mediawiki/XophpStringRef.java new file mode 100644 index 000000000..11f6dd46b --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpStringRef.java @@ -0,0 +1,21 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpStringRef { + private String val; + public XophpStringRef(String val) {this.val = val;} + public String Get() {return val;} +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java index 66892c6d9..0bc04fca1 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java @@ -18,9 +18,13 @@ import gplx.core.btries.*; import gplx.core.intls.*; import gplx.objects.strings.unicodes.*; import gplx.core.primitives.*; +import gplx.objects.strings.bfrs.*; public class XophpString_ implements XophpCallbackOwner { public static final String False = null; - public static boolean is_true(String s) {return s != null;} // handles code like "if ($var)" where var is an Object; + public static boolean is_true (String s) {return s != null;} // handles code like "if ($var)" where var is an Object; + public static boolean is_false(String s) {return s == null;} + public static boolean eq(String lhs, String rhs) {return String_.Eq(lhs, rhs);} + public static boolean eq_not(String lhs, String rhs) {return !String_.Eq(lhs, rhs);} // REF.PHP: https://www.php.net/manual/en/function.strpos.php public static int strpos(String haystack, String needle) {return strpos(haystack, needle, 0);} @@ -80,16 +84,36 @@ public class XophpString_ implements XophpCallbackOwner { } return rv; } - public static int strspn(String subject, Hash_adp mask, int start) {return strspn(subject, mask, start, Int_.Null);} + public static int strspn(String subject, Hash_adp mask, int start) {return strspn(subject, mask, start, Int_.Zero);} public static int strspn(String subject, Hash_adp mask, int start, int length) { int subject_len = String_.Len(subject); - - // get subject_end + start = strspn__start(start, subject_len); + int subject_end = strspn__subject_end(start, length, subject_len); + return strspn__rslt(Bool_.Y, subject, mask, start, subject_end); + } + // REF.PHP:https://www.php.net/manual/en/function.strcspn.php + public static int strcspn(String subject, Hash_adp mask) {return strcspn(subject, mask, Int_.Zero, Int_.Zero);} + public static int strcspn(String subject, Hash_adp mask, int start) {return strcspn(subject, mask, start, Int_.Zero);} + public static int strcspn(String subject, Hash_adp mask, int start, int length) { + int subject_len = String_.Len(subject); + start = strspn__start(start, subject_len); + int subject_end = strspn__subject_end(start, length, subject_len); + return strspn__rslt(Bool_.N, subject, mask, start, subject_end); + } + private static int strspn__start(int start, int subject_len) { + if (start < 0) { // adjust start if -1 + start = subject_len + start; + if (start < 0) + start = 0; + } + return start; + } + private static int strspn__subject_end(int start, int length, int subject_len) { int subject_end = 0; - if (length == Int_.Null) { + if (length == Int_.Zero) { subject_end = subject_len; } - else if (length < 0) { + else if (length < Int_.Zero) { subject_end = subject_len + length; // If length is given and is negative, then subject will be examined from the starting position up to length characters from the end of subject. if (subject_end < start) subject_end = start; @@ -99,9 +123,11 @@ public class XophpString_ implements XophpCallbackOwner { if (subject_end > subject_len) subject_end = subject_len; } - + return subject_end; + } + private static int strspn__rslt(boolean is_strspn, String subject, Hash_adp mask, int start, int subject_end) { // loop subject until encountering character not in mask - int rv = 0; + int strspn_rv = 0; int i = start; while (i < subject_end) { char subject_char = String_.CharAt(subject, i); @@ -117,14 +143,21 @@ public class XophpString_ implements XophpCallbackOwner { } if (mask.Has(mask_key)) { - rv++; + if (is_strspn) { + strspn_rv++; + } + else { + break; + } } else { - break; + if (is_strspn) { + break; + } } i++; } - return rv; + return is_strspn ? strspn_rv : i - start; } public static int strspn_fwd__ary(byte[] src, boolean[] find, int bgn, int max, int src_len) { if (max == -1) max = src_len; @@ -389,17 +422,15 @@ public class XophpString_ implements XophpCallbackOwner { rv[rv_idx] = trim_pos; } } - public static String str_repeat(String val, int count) { - int val_len = String_.Len(val); - int chry_len = val_len * count; - char[] chry = new char[chry_len]; - for (int i = 0; i < count; i++) { - for (int j = 0; j < val_len; j++) { - chry[(i * val_len) + j] = String_.CharAt(val, j); - } + // REF.PHP: https://www.php.net/manual/en/function.str-repeat.php + public static String str_repeat(String input, int multiplier) { + String_bfr sb = new String_bfr(); + for (int i = 0; i < multiplier; i++) { + sb.Add(input); } - return String_.new_charAry_(chry, 0, chry_len); + return sb.To_str_and_clear(); } + public static boolean is_string(Object o) { return String_.as_(o) != null; } @@ -512,6 +543,26 @@ public class XophpString_ implements XophpCallbackOwner { return b >= 128 && b <= 255; } } + + // REF.PHP: https://www.php.net/manual/en/function.strrev.php + public static String strrev(String src) { + String_bfr sb = new String_bfr(); + Ustring usrc = Ustring_.New_codepoints(src); + int usrc_len = usrc.Len_in_data(); + for (int i = usrc_len - 1; i > -1; i--) { + int c = usrc.Get_data(i); + sb.Add_char_by_code(c); + } + return sb.To_str_and_clear(); + } + + public static String Char_as_str(String s, int idx) { + return Char_.To_str(String_.CharAt(s, idx)); + } + public static boolean Char_eq(String s, int idx, String comp) { + return String_.Eq(Char_as_str(s, idx), comp); + } + public Object Callback(String method, Object... args) { if (String_.Eq(method, "strtoupper")) { String val = (String)args[0]; diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java index 72ff9fd0e..db7de50d6 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java @@ -60,12 +60,6 @@ public class XophpString__tst { fxt.Test__strpos("aba", "a", 1, 2); fxt.Test__strpos("aba", "a", -2, 2); } - @Test public void strspn() { - fxt.Test__strspn("42 is the answer to the 128th question", fxt.Init__strspn_hash("1234567890"), 0, Int_.Min_value, 2); - fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 0, Int_.Min_value, 0); - fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 1, 2, 2); - fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 1, 1, 1); - } @Test public void rtrim() { // pad is 0, 1 char fxt.Test__rtrim("0100", "", "0100"); // empty pad returns String @@ -151,6 +145,36 @@ public class XophpString__tst { fxt.Test__Fmt("a$0b", "a$0b", "z"); // invalid identifier fxt.Test__Fmt("a0", "a$xyz0b", "0"); // long identifier } + @Test public void strrev() { + fxt.Test__strrev("", ""); + fxt.Test__strrev("Hello world!", "!dlrow olleH"); + fxt.Test__strrev("☆❤world", "dlrow❤☆"); + fxt.Test__strrev("a¢€𤭢b", "b𤭢€¢a"); + } + @Test public void str_repeat() { + fxt.Test__str_repeat("-=", 10, "-=-=-=-=-=-=-=-=-=-="); + } + @Test public void strspn() { + fxt.Test__strspn(1, "<-", "abc", "abc", -1); + + // PHP samples + fxt.Test__strspn(2, "<-", "42 is the answer to the 128th question", "1234567890"); + fxt.Test__strspn(0, "<-", "foo", "o"); + fxt.Test__strspn(2, "<-", "foo", "o", 1, 2); + fxt.Test__strspn(1, "<-", "foo", "o", 1, 1); + } + @Test public void strcspn() { + fxt.Test__strcspn(3, "abc", "x", 0); + fxt.Test__strcspn(3, "abc", "x", -5); + + // PHP samples + fxt.Test__strcspn(0, "abcd", "apple"); + fxt.Test__strcspn(0, "abcd", "banana"); + fxt.Test__strcspn(2, "hello", "l"); + fxt.Test__strcspn(2, "hello", "world"); + fxt.Test__strcspn(5, "abcdhelloabcd", "abcd", -9); + fxt.Test__strcspn(4, "abcdhelloabcd", "abcd", -9, -5); + } } class XophpString__fxt { public void Test_strspn_fwd__byte(String src_str, byte find, int bgn, int max, int expd) { @@ -191,8 +215,10 @@ class XophpString__fxt { Gftest.Eq__int(expd, XophpString_.strpos(haystack, needle, offset)); } public Hash_adp Init__strspn_hash(String mask) {return XophpString_.strspn_hash(mask);} - public void Test__strspn(String subject, Hash_adp mask, int start, int length, int expd) { - int actl = XophpString_.strspn(subject, mask, start, length); + public void Test__strspn(int expd, String ignore, String subject, String mask) {Test__strspn(expd, ignore, subject, mask, Int_.Zero, Int_.Zero);} + public void Test__strspn(int expd, String ignore, String subject, String mask, int start) {Test__strspn(expd, ignore, subject, mask, start, Int_.Zero);} + public void Test__strspn(int expd, String ignore, String subject, String mask, int start, int length) { + int actl = XophpString_.strspn(subject, XophpString_.strspn_hash(mask), start, length); Gftest.Eq__int(expd, actl); } public void Test__rtrim(String str, String character_mask, String expd) { @@ -222,4 +248,15 @@ class XophpString__fxt { public void Test__Fmt(String expd, String fmt, Object... args) { Gftest.Eq__str(expd, XophpString_.Fmt(fmt, args)); } + public void Test__strrev(String src, String expd) { + Gftest.Eq__str(expd, XophpString_.strrev(src)); + } + public void Test__str_repeat(String input, int multiplier, String expd) { + Gftest.Eq__str(expd, XophpString_.str_repeat(input, multiplier)); + } + public void Test__strcspn(int expd, String subject, String mask) {Test__strcspn(expd, subject, mask, Int_.Zero, Int_.Zero);} + public void Test__strcspn(int expd, String subject, String mask, int start) {Test__strcspn(expd, subject, mask, start, Int_.Zero);} + public void Test__strcspn(int expd, String subject, String mask, int start, int length) { + Gftest.Eq__int(expd, XophpString_.strcspn(subject, XophpString_.strspn_hash(mask), start, length)); + } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpType_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpType_.java index 42a809b68..b0b10af55 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpType_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpType_.java @@ -49,6 +49,6 @@ public class XophpType_ { return Type_.Eq_by_obj(o, XophpArray.class); } public static boolean instance_of(Object o, Class t) { - return Type_.Eq_by_obj(o, t); + return Type_.Eq_by_obj(o, t) || Type_.Is_assignable_from_by_obj(o, t); } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/JsonConfig/includes/JCContentHandler.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/JsonConfig/includes/JCContentHandler.java index 416d30a2c..e3073df88 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/extensions/JsonConfig/includes/JCContentHandler.java +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/JsonConfig/includes/JCContentHandler.java @@ -25,7 +25,7 @@ class JCContentHandler extends TextContentHandler { /** * @param String $modelId */ public void __construct(String modelId, JCSingleton singleton) { - super.__construct(modelId, XomwDefines.CONTENT_FORMAT_JSON, CONTENT_FORMAT_JSON_PRETTY); + super.__construct(modelId, XophpArray.New(XomwDefines.CONTENT_FORMAT_JSON, CONTENT_FORMAT_JSON_PRETTY)); this.singleton = singleton; } diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwDefaultSettings.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwDefaultSettings.java index 04fa053a0..de0b677a8 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwDefaultSettings.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwDefaultSettings.java @@ -3036,7 +3036,7 @@ public class XomwDefaultSettings { // /** // * Whether to enable language variant conversion. // */ -// $wgDisableLangConversion = false; + public static boolean wgDisableLangConversion = false; // // /** // * Whether to enable language variant conversion for links. @@ -4146,19 +4146,19 @@ public class XomwDefaultSettings { // * When the limit is exceeded, an exception is thrown. // */ // $wgMaxGeneratedPPNodeCount = 1000000; -// -// /** -// * Maximum recursion depth for templates within templates. -// * The current parser adds two levels to the PHP call stack for each template, -// * and xdebug limits the call stack to 100 by default. So this should hopefully -// * stop the parser before it hits the xdebug limit. -// */ -// $wgMaxTemplateDepth = 40; + + /** + * Maximum recursion depth for templates within templates. + * The current parser adds two levels to the PHP call stack for each template, + * and xdebug limits the call stack to 100 by default. So this should hopefully + * stop the parser before it hits the xdebug limit. + */ + public static final int wgMaxTemplateDepth = 40; /** * @see $wgMaxTemplateDepth */ - public static int wgMaxPPExpandDepth = 40; + public static final int wgMaxPPExpandDepth = 40; // /** // * URL schemes that should be recognized as valid by wfParseUrl(). diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java index 0b7be2160..8884fde3e 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java @@ -252,7 +252,8 @@ public class XomwTitle { * @throws InvalidArgumentException * @return Title|null Title or null on an error. */ - public static XomwTitle newFromText(XomwEnv env, byte[] text) {return newFromText(env, text, XomwDefines.NS_MAIN);} + public static XomwTitle newFromText(XomwEnv env, byte[] text) {return newFromText(env, text, XomwDefines.NS_MAIN);} + public static XomwTitle newFromText(XomwEnv env, String text, int defaultNamespace) {return newFromText(env, Bry_.new_u8(text));} private static XomwTitle newFromText(XomwEnv env, byte[] text, int defaultNamespace) { // DWIM: Integers can be passed in here when page titles are used as array keys. // XO.MW.SKIP:STRONGCAST @@ -1454,6 +1455,7 @@ public class XomwTitle { } return this.mPrefixedText; } + public String getPrefixedTextStr() {return String_.new_u8(getPrefixedText());} // /** // * Return a String representation of this title diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/content/ContentHandler.java b/400_xowa/src/gplx/xowa/mediawiki/includes/content/ContentHandler.java index b613800db..eb753a809 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/content/ContentHandler.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/content/ContentHandler.java @@ -358,7 +358,7 @@ public abstract class ContentHandler { /** * @var String[] */ - protected String[] mSupportedFormats; + protected XophpArray mSupportedFormats; /** * Constructor, initializing the ContentHandler instance with its model ID @@ -369,7 +369,7 @@ public abstract class ContentHandler { * @param String[] $formats List for supported serialization formats * (typically as MIME types) */ - @gplx.Virtual public void __construct(String modelId, String... formats) { + @gplx.Virtual public void __construct(String modelId, XophpArray formats) { this.mModelID = modelId; this.mSupportedFormats = formats; } @@ -495,7 +495,7 @@ public abstract class ContentHandler { * * @return String[] List of serialization formats as MIME type like strings */ - public String[] getSupportedFormats() { + public XophpArray getSupportedFormats() { return this.mSupportedFormats; } diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/content/TextContentHandler.java b/400_xowa/src/gplx/xowa/mediawiki/includes/content/TextContentHandler.java index 0cd14f6e4..d3c16c6f9 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/content/TextContentHandler.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/content/TextContentHandler.java @@ -14,13 +14,11 @@ GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki.includes.content; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; -public class TextContentHandler extends ContentHandler { // @codingStandardsIgnoreStart bug 57585 - public void __construct() {this.__construct(XomwDefines.CONTENT_MODEL_TEXT, XomwDefines.CONTENT_FORMAT_TEXT);} - @Override public void __construct(String modelId, String... formats) { +public class TextContentHandler extends ContentHandler { public void __construct() {this.__construct(XomwDefines.CONTENT_MODEL_TEXT, XophpArray.New(XomwDefines.CONTENT_FORMAT_TEXT));} + @Override public void __construct(String modelId, XophpArray formats) { super.__construct(modelId, formats); } - // @codingStandardsIgnoreEnd -// + // /** // * Returns the content's text as-is. // * diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwParser.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwParser.java index a5afd893f..bbc502b7b 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwParser.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwParser.java @@ -143,7 +143,7 @@ public class XomwParser implements XomwParserIface { // public mFunctionHooks = []; // public mFunctionSynonyms = [ 0 => [], 1 => [] ]; // public mFunctionTagHooks = []; -// public mStripList = []; + public XophpArray mStripList = XophpArray.New(); // public mDefaultStripList = []; // public mVarCache = []; // public mImageParams = []; @@ -295,7 +295,7 @@ public class XomwParser implements XomwParserIface { // XomwParser.EXT_LINK_ADDR . // XomwParser.EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su'; - this.mPreprocessorClass = XomwPreprocessor_DOM.Instance; + this.mPreprocessorClass = XomwPreprocessor_Hash.Instance; // if (isset($conf['preprocessorClass'])) { // this.mPreprocessorClass = $conf['preprocessorClass']; // } elseif (defined('HPHP_VERSION')) { @@ -1096,15 +1096,15 @@ public class XomwParser implements XomwParserIface { // } // return $stripped; // } -// -// /** -// * Get a list of strippable XML-like elements -// * -// * @return array -// */ -// public function getStripList() { -// return this.mStripList; -// } + + /** + * Get a list of strippable XML-like elements + * + * @return array + */ + public XophpArray getStripList() { + return this.mStripList; + } /** * Add an item to the strip state @@ -1147,8 +1147,8 @@ public class XomwParser implements XomwParserIface { * @return String */ // isMain=tru - public void internalParse(XomwParserBfr pbfr, XomwParserCtx pctx, byte[] text) {internalParse(pbfr, pctx, text, true, false);} - public void internalParse(XomwParserBfr pbfr, XomwParserCtx pctx, byte[] text, boolean isMain, boolean frame) { + public void internalParse(XomwParserBfr pbfr, XomwParserCtx pctx, byte[] text) {internalParse(pbfr, pctx, text, true, null);} + public void internalParse(XomwParserBfr pbfr, XomwParserCtx pctx, byte[] text, boolean isMain, XomwPPFrame frame) { pbfr.Init(text); // $origText = text; @@ -1206,6 +1206,69 @@ public class XomwParser implements XomwParserIface { // $text = $this->formatHeadings($text, $origText, $isMain); } + public String internalParse(String text, boolean isMain, XomwPPFrame frame) { // isMain=true; frame=false; + // String origText = text; + + // Avoid PHP 7.1 warning from passing this by reference + // XomwParser parser = this; + + // Hook to suspend the parser in this state +// if (!Hooks::run("ParserBeforeInternalParse", [ &parser, &text, &this.mStripState ])) { +// return text; +// } + + // if frame is provided, then use frame for replacing any variables + int flag = 0; + if (XophpObject_.is_true(frame)) { + // use frame depth to infer how include/noinclude tags should be handled + // depth=0 means this is the top-level document; otherwise it's an included document + if (!XophpInt_.is_true(frame.depth)) { + flag = 0; + } else { + flag = XomwParser.PTD_FOR_INCLUSION; + } + XomwPPNode dom = this.preprocessToDom(text, flag); + text = frame.expand(dom); + } else { + // if frame is not provided, then use old-style replaceVariables +// text = this.replaceVariables(text); + } + +// Hooks::run("InternalParseBeforeSanitize", [ &parser, &text, &this.mStripState ]); +// text = Sanitizer::removeHTMLtags( +// text, +// [ this, "attributeStripCallback" ], +// false, +// array_keys(this.mTransparentTagHooks), +// [], +// [ this, "addTrackingCategory" ] +// ); +// Hooks::run("InternalParseBeforeLinks", [ &parser, &text, &this.mStripState ]); + + // Tables need to come after variable replacement for things to work + // properly; putting them before other transformations should keep + // exciting things like link expansions from showing up in surprising + // places. +// text = this.doTableStuff(text); +// +// text = preg_replace("/(^|\n)-----* /", "\\1
", text); +// +// text = this.doDoubleUnderscore(text); +// +// text = this.doHeadings(text); +// text = this.replaceInternalLinks(text); +// text = this.doAllQuotes(text); +// text = this.replaceExternalLinks(text); +// +// // replaceInternalLinks may sometimes leave behind +// // absolute URLs, which have to be masked to hide them from replaceExternalLinks +// text = str_replace(self::MARKER_PREFIX . "NOPARSE", "", text); +// +// text = this.doMagicLinks(text); +// text = this.formatHeadings(text, origText, isMain); + + return text; + } /** * Helper function for parse() that transforms half-parsed HTML into fully * parsed HTML. @@ -2286,329 +2349,356 @@ public class XomwParser implements XomwParserIface { // this.addTrackingCategory("$limitationType-category"); // } // -// /** -// * Return the text of a template, after recursively -// * replacing any variables or templates within the template. -// * -// * @param array $piece The parts of the template -// * $piece['title']: the title, i.e. the part before the | -// * $piece['parts']: the parameter array -// * $piece['lineStart']: whether the brace was at the start of a line -// * @param PPFrame $frame The current frame, contains template arguments -// * @throws Exception -// * @return String The text of the template -// */ + // MW.SRC:1.33.1 + /** + * Return the text of a template, after recursively + * replacing any variables or templates within the template. + * + * @param array $piece The parts of the template + * $piece['title']: the title, i.e. the part before the | + * $piece['parts']: the parameter array + * $piece['lineStart']: whether the brace was at the start of a line + * @param PPFrame $frame The current frame, contains template arguments + * @throws Exception + * @return String The text of the template + */ public XophpArray braceSubstitution(XophpArray piece, XomwPPFrame frame) { -// -// // Flags -// -// // $text has been filled -// $found = false; -// // wiki markup in $text should be escaped -// $nowiki = false; -// // $text is HTML, armour it against wikitext transformation -// $isHTML = false; -// // Force interwiki transclusion to be done in raw mode not rendered -// $forceRawInterwiki = false; -// // $text is a DOM node needing expansion in a child frame -// $isChildObj = false; -// // $text is a DOM node needing expansion in the current frame -// $isLocalObj = false; -// -// // Title Object, where $text came from -// $title = false; -// -// // $part1 is the bit before the first |, and must contain only title characters. -// // Various prefixes will be stripped from it later. -// $titleWithSpaces = $frame->expand($piece['title']); -// $part1 = trim($titleWithSpaces); -// $titleText = false; -// -// // Original title text preserved for various purposes -// $originalTitle = $part1; -// -// // $args is a list of argument nodes, starting from index 0, not including $part1 -// // @todo FIXME: If piece['parts'] is null then the call to getLength() -// // below won't work b/c this $args isn't an Object -// $args = (null == $piece['parts']) ? [] : $piece['parts']; -// -// $profileSection = null; // profile templates -// -// // SUBST -// if (!$found) { -// $substMatch = this.mSubstWords->matchStartAndRemove($part1); + // Flags + + // text has been filled + boolean found = false; + // wiki markup in text should be escaped + boolean nowiki = false; + // text is HTML, armour it against wikitext transformation + boolean isHTML = false; + // Force interwiki transclusion to be done in raw mode not rendered + boolean forceRawInterwiki = false; + // text is a DOM node needing expansion in a child frame + boolean isChildObj = false; + // text is a DOM node needing expansion in the current frame + boolean isLocalObj = false; + + // Title Object, where text came from + XomwTitle title = null; + + // part1 is the bit before the first |, and must contain only title characters. + // Various prefixes will be stripped from it later. + String titleWithSpaces = frame.expand(piece.Get_by("title")); + String part1 = XophpString_.trim(titleWithSpaces); + String titleText = XophpString_.False; + + // Original title text preserved for various purposes + String originalTitle = part1; + + // args is a list of argument nodes, starting from index 0, not including part1 + // @todo FIXME: If piece["parts"] is null then the call to getLength() + // below won"t work b/c this args isn"t an Object + XomwPPNode args = (piece.Get_by("parts") == null) ? new XomwPPNode_Hash_Array(XophpArray.New()) : (XomwPPNode_Hash_Array)piece.Get_by("parts"); + +// boolean profileSection = XophpBool_.Null; // profile templates + + String text = null; + // SUBST + if (!found) { +// substMatch = this.mSubstWords.matchStartAndRemove(part1); // // // Possibilities for substMatch: "subst", "safesubst" or FALSE // // Decide whether to expand template or keep wikitext as-is. -// if (this.ot['wiki']) { -// if ($substMatch === false) { -// $literal = true; // literal when in PST with no prefix +// if (this.ot["wiki"]) { +// if (substMatch === false) { +// literal = true; # literal when in PST with no prefix // } else { -// $literal = false; // expand when in PST with subst: or safesubst: +// literal = false; # expand when in PST with subst: or safesubst: // } // } else { -// if ($substMatch == 'subst') { -// $literal = true; // literal when not in PST with plain subst: +// if (substMatch == "subst") { +// literal = true; # literal when not in PST with plain subst: // } else { -// $literal = false; // expand when not in PST with safesubst: or no prefix +// literal = false; # expand when not in PST with safesubst: or no prefix // } // } -// if ($literal) { -// $text = $frame->virtualBracketedImplode('{{', '|', '}}', $titleWithSpaces, $args); -// $isLocalObj = true; -// $found = true; +// if (literal) { +// text = frame.virtualBracketedImplode("{{", "|", "}}", titleWithSpaces, args); +// isLocalObj = true; +// found = true; // } -// } -// + } + // // Variables -// if (!$found && $args->getLength() == 0) { -// $id = this.mVariables->matchStartToEnd($part1); -// if ($id !== false) { -// $text = this.getVariableValue($id, $frame); -// if (MagicWord::getCacheTTL($id) > -1) { -// this.mOutput->updateCacheExpiry(MagicWord::getCacheTTL($id)); +// if (!found && args.getLength() == 0) { +// id = this.mVariables.matchStartToEnd(part1); +// if (id !== false) { +// text = this.getVariableValue(id, frame); +// if (this.magicWordFactory.getCacheTTL(id) > -1) { +// this.mOutput.updateCacheExpiry( +// this.magicWordFactory.getCacheTTL(id)); // } -// $found = true; +// found = true; // } // } // // // MSG, MSGNW and RAW -// if (!$found) { +// if (!found) { // // Check for MSGNW: -// mwMsgnw = MagicWord::get('msgnw'); -// if (mwMsgnw->matchStartAndRemove($part1)) { -// $nowiki = true; +// mwMsgnw = this.magicWordFactory.get("msgnw"); +// if (mwMsgnw.matchStartAndRemove(part1)) { +// nowiki = true; // } else { // // Remove obsolete MSG: -// mwMsg = MagicWord::get('msg'); -// mwMsg->matchStartAndRemove($part1); +// mwMsg = this.magicWordFactory.get("msg"); +// mwMsg.matchStartAndRemove(part1); // } // // // Check for RAW: -// mwRaw = MagicWord::get('raw'); -// if (mwRaw->matchStartAndRemove($part1)) { -// $forceRawInterwiki = true; -// } -// } -// -// // Parser functions -// if (!$found) { -// $colonPos = strpos($part1, ':'); -// if ($colonPos !== false) { -// $func = substr($part1, 0, $colonPos); -// $funcArgs = [ trim(substr($part1, $colonPos + 1)) ]; -// $argsLength = $args->getLength(); -// for ($i = 0; $i < $argsLength; $i++) { -// $funcArgs[] = $args->item($i); -// } -// try { -// $result = this.callParserFunction($frame, $func, $funcArgs); -// } catch (Exception $ex) { -// throw $ex; -// } -// -// // The interface for parser functions allows for extracting -// // flags into the local scope. Extract any forwarded flags -// // here. -// extract($result); +// mwRaw = this.magicWordFactory.get("raw"); +// if (mwRaw.matchStartAndRemove(part1)) { +// forceRawInterwiki = true; // } // } -// -// // Finish mangling title and then check for loops. -// // Set $title to a Title Object and $titleText to the PDBK -// if (!$found) { -// $ns = NS_TEMPLATE; -// // Split the title into page and subpage -// $subpage = ''; -// $relative = this.maybeDoSubpageLink($part1, $subpage); -// if ($part1 !== $relative) { -// $part1 = $relative; -// $ns = this.mTitle->getNamespace(); -// } -// $title = Title::newFromText($part1, $ns); -// if ($title) { -// $titleText = $title->getPrefixedText(); -// // Check for language variants if the template is not found -// if (this.getConverterLanguage()->hasVariants() && $title->getArticleID() == 0) { -// this.getConverterLanguage()->findVariantLink($part1, $title, true); -// } -// // Do recursion depth check -// $limit = this.mOptions->getMaxTemplateDepth(); -// if ($frame->depth >= $limit) { -// $found = true; -// $text = '' -// . wfMessage('parser-template-recursion-depth-warning') -// ->numParams($limit)->inContentLanguage()->text() -// . ''; + + // Parser functions + if (!found) { + int colonPos = XophpString_.strpos(part1, ":"); + if (!XophpInt_.is_false(colonPos)) { +// String func = XophpString_.substr(part1, 0, colonPos); + XophpArray funcArgs = XophpArray.New(XophpString_.trim(XophpString_.substr(part1, colonPos + 1))); + int argsLength = args.getLength(); + for (int i = 0; i < argsLength; i++) { + funcArgs.Add(args.item(i)); + } + + XophpArray result = XophpArray.New(); +// result = this.callParserFunction(frame, func, funcArgs); + + // Extract any forwarded flags + if (XophpArray_.isset(result, "title")) { + title = (XomwTitle)result.Get_by("title"); + } + if (XophpArray_.isset(result, "found")) { + found = result.Get_by_bool("found"); + } + if (XophpArray_.array_key_exists("text", result)) { + // a String or null + text = result.Get_by_str("text"); + } + if (XophpArray_.isset(result, "nowiki")) { + nowiki = result.Get_by_bool("nowiki"); + } + if (XophpArray_.isset(result, "isHTML")) { + isHTML = result.Get_by_bool("isHTML"); + } + if (XophpArray_.isset(result, "forceRawInterwiki")) { + forceRawInterwiki = result.Get_by_bool("forceRawInterwiki"); + } + if (XophpArray_.isset(result, "isChildObj")) { + isChildObj = result.Get_by_bool("isChildObj"); + } + if (XophpArray_.isset(result, "isLocalObj")) { + isLocalObj = result.Get_by_bool("isLocalObj"); + } + } + } + + // Finish mangling title and then check for loops. + // Set title to a Title Object and titleText to the PDBK + if (!found) { + int ns = XomwDefines.NS_TEMPLATE; + // Split the title into page and subpage +// String subpage = ""; +// String relative = this.maybeDoSubpageLink(part1, subpage); + String relative = ""; + + if (!String_.Eq(part1, relative)) { + part1 = relative; + ns = this.mTitle.getNamespace(); + } + title = XomwTitle.newFromText(env, part1, ns); + if (XophpObject_.is_true(title)) { + titleText = title.getPrefixedTextStr(); + // Check for language variants if the template is not found +// if (this.getTargetLanguage().hasVariants() && title.getArticleID() == 0) { +// this.getTargetLanguage().findVariantLink(part1, title, true); // } -// } -// } -// -// // Load from database -// if (!$found && $title) { -// $profileSection = this.mProfiler->scopedProfileIn($title->getPrefixedDBkey()); -// if (!$title->isExternal()) { -// if ($title->isSpecialPage() -// && this.mOptions->getAllowSpecialInclusion() -// && this.ot['html'] + // Do recursion depth check + int limit = this.mOptions.getMaxTemplateDepth(); + if (frame.depth >= limit) { + found = true; +// text = "" +// + wfMessage("parser-template-recursion-depth-warning") +// .numParams(limit).inContentLanguage().text() +// + ""; + } + } + } + + // Load from database + if (!found && XophpObject_.is_true(title)) { +// profileSection = this.mProfiler.scopedProfileIn(title.getPrefixedDBkey()); +// if (!title.isExternal()) { +// if (title.isSpecialPage() +// && this.mOptions.getAllowSpecialInclusion() +// && this.ot["html"] // ) { -// $specialPage = SpecialPageFactory::getPage($title->getDBkey()); +// specialPage = this.specialPageFactory.getPage(title.getDBkey()); // // Pass the template arguments as URL parameters. // // "uselang" will have no effect since the Language Object // // is forced to the one defined in ParserOptions. -// $pageArgs = []; -// $argsLength = $args->getLength(); -// for ($i = 0; $i < $argsLength; $i++) { -// $bits = $args->item($i)->splitArg(); -// if (strval($bits['index']) === '') { -// $name = trim($frame->expand($bits['name'], PPFrame::STRIP_COMMENTS)); -// $value = trim($frame->expand($bits['value'])); -// $pageArgs[$name] = $value; +// pageArgs = []; +// argsLength = args.getLength(); +// for (i = 0; i < argsLength; i++) { +// bits = args.item(i).splitArg(); +// if (strval(bits["index"]) === "") { +// name = trim(frame.expand(bits["name"], PPFrame::STRIP_COMMENTS)); +// value = trim(frame.expand(bits["value"])); +// pageArgs[name] = value; // } // } // // // Create a new context to execute the special page -// $context = new RequestContext; -// $context->setTitle($title); -// $context->setRequest(new FauxRequest($pageArgs)); -// if ($specialPage && $specialPage->maxIncludeCacheTime() === 0) { -// $context->setUser(this.getUser()); +// context = new RequestContext; +// context.setTitle(title); +// context.setRequest(new FauxRequest(pageArgs)); +// if (specialPage && specialPage.maxIncludeCacheTime() === 0) { +// context.setUser(this.getUser()); // } else { // // If this page is cached, then we better not be per user. -// $context->setUser(User::newFromName('127.0.0.1', false)); +// context.setUser(User::newFromName("127.0.0.1", false)); // } -// $context->setLanguage(this.mOptions->getUserLangObj()); -// $ret = SpecialPageFactory::capturePath( -// $title, $context, this.getLinkRenderer()); -// if ($ret) { -// $text = $context->getOutput()->getHTML(); -// this.mOutput->addOutputPageMetadata($context->getOutput()); -// $found = true; -// $isHTML = true; -// if ($specialPage && $specialPage->maxIncludeCacheTime() !== false) { -// this.mOutput->updateRuntimeAdaptiveExpiry( -// $specialPage->maxIncludeCacheTime() +// context.setLanguage(this.mOptions.getUserLangObj()); +// ret = this.specialPageFactory.capturePath(title, context, this.getLinkRenderer()); +// if (ret) { +// text = context.getOutput().getHTML(); +// this.mOutput.addOutputPageMetadata(context.getOutput()); +// found = true; +// isHTML = true; +// if (specialPage && specialPage.maxIncludeCacheTime() !== false) { +// this.mOutput.updateRuntimeAdaptiveExpiry( +// specialPage.maxIncludeCacheTime() // ); // } // } -// } elseif (MWNamespace::isNonincludable($title->getNamespace())) { -// $found = false; // access denied +// } elseif (MWNamespace::isNonincludable(title.getNamespace())) { +// found = false; # access denied // wfDebug(__METHOD__ . ": template inclusion denied for " . -// $title->getPrefixedDBkey() . "\n"); +// title.getPrefixedDBkey() . "\n"); // } else { -// list($text, $title) = this.getTemplateDom($title); -// if ($text !== false) { -// $found = true; -// $isChildObj = true; +// list(text, title) = this.getTemplateDom(title); +// if (text !== false) { +// found = true; +// isChildObj = true; // } // } // // // If the title is valid but undisplayable, make a link to it -// if (!$found && (this.ot['html'] || this.ot['pre'])) { -// $text = "[[:$titleText]]"; -// $found = true; +// if (!found && (this.ot["html"] || this.ot["pre"])) { +// text = "[[:titleText]]"; +// found = true; // } -// } elseif ($title->isTrans()) { +// } elseif (title.isTrans()) { // // Interwiki transclusion -// if (this.ot['html'] && !$forceRawInterwiki) { -// $text = this.interwikiTransclude($title, 'render'); -// $isHTML = true; +// if (this.ot["html"] && !forceRawInterwiki) { +// text = this.interwikiTransclude(title, "render"); +// isHTML = true; // } else { -// $text = this.interwikiTransclude($title, 'raw'); +// text = this.interwikiTransclude(title, "raw"); // // Preprocess it like a template -// $text = this.preprocessToDom($text, XomwParser.PTD_FOR_INCLUSION); -// $isChildObj = true; +// text = this.preprocessToDom(text, self::PTD_FOR_INCLUSION); +// isChildObj = true; // } -// $found = true; -// } -// -// // Do infinite loop check -// // This has to be done after redirect resolution to avoid infinite loops via redirects -// if (!$frame->loopCheck($title)) { -// $found = true; -// $text = '' -// . wfMessage('parser-template-loop-warning', $titleText)->inContentLanguage()->text() -// . ''; -// wfDebug(__METHOD__ . ": template loop broken at '$titleText'\n"); +// found = true; // } -// } -// -// // If we haven't found text to substitute by now, we're done -// // Recover the source wikitext and return it -// if (!$found) { -// $text = $frame->virtualBracketedImplode('{{', '|', '}}', $titleWithSpaces, $args); -// if ($profileSection) { -// this.mProfiler->scopedProfileOut($profileSection); + + // Do infinite loop check + // This has to be done after redirect resolution to avoid infinite loops via redirects + if (!frame.loopCheck(title)) { + found = true; +// text = "" +// + wfMessage("parser-template-loop-warning", titleText).inContentLanguage().text() +// + ""; +// this.addTrackingCategory("template-loop-category"); +// this.mOutput.addWarning(wfMessage("template-loop-warning", +// wfEscapeWikiText(titleText)).text()); +// wfDebug(__METHOD__ . ": template loop broken at "titleText"\n"); + } + } + + // If we haven"t found text to substitute by now, we"re done + // Recover the source wikitext and return it +// if (!found) { +// text = frame.virtualBracketedImplode("{{", "|", "}}", titleWithSpaces, args); +// if (profileSection) { +// this.mProfiler.scopedProfileOut(profileSection); // } -// return [ 'Object' => $text ]; +// return ["Object", text]; // } -// + +// XomwPPFrame newFrame = null; // // Expand DOM-style return values in a child frame -// if ($isChildObj) { +// if (isChildObj) { // // Clean up argument array -// $newFrame = $frame->newChild($args, $title); +// newFrame = frame.newChild(args, title); // -// if ($nowiki) { -// $text = $newFrame->expand($text, PPFrame::RECOVER_ORIG); -// } elseif ($titleText !== false && $newFrame->isEmpty()) { +// if (nowiki) { +// text = newFrame.expand(text, XomwPPFrame.RECOVER_ORIG); +// } else if (!XophpString_.is_false(titleText) && newFrame.isEmpty()) { // // Expansion is eligible for the empty-frame cache -// $text = $newFrame->cachedExpand($titleText, $text); +// text = newFrame.cachedExpand(titleText, text); // } else { // // Uncached expansion -// $text = $newFrame->expand($text); +// text = newFrame.expand(text); // } // } -// if ($isLocalObj && $nowiki) { -// $text = $frame->expand($text, PPFrame::RECOVER_ORIG); -// $isLocalObj = false; +// if (isLocalObj && nowiki) { +// text = frame.expand(text, PPFrame::RECOVER_ORIG); +// isLocalObj = false; // } // -// if ($profileSection) { -// this.mProfiler->scopedProfileOut($profileSection); +// if (profileSection) { +// this.mProfiler.scopedProfileOut(profileSection); // } // // // Replace raw HTML by a placeholder -// if ($isHTML) { -// $text = this.insertStripItem($text); -// } elseif ($nowiki && (this.ot['html'] || this.ot['pre'])) { +// if (isHTML) { +// text = this.insertStripItem(text); +// } elseif (nowiki && (this.ot["html"] || this.ot["pre"])) { // // Escape nowiki-style return values -// $text = wfEscapeWikiText($text); -// } elseif (is_string($text) -// && !$piece['lineStart'] -// && preg_match('/^(?:{\\||:|;|#|\*)/', $text) +// text = wfEscapeWikiText(text); +// } elseif (is_string(text) +// && !piece["lineStart"] +// && preg_match("/^(?:{\\||:|;|#|\*)/", text) // ) { // // T2529: if the template begins with a table or block-level // // element, it should be treated as beginning a new line. // // This behavior is somewhat controversial. -// $text = "\n" . $text; +// text = "\n" . text; // } // -// if (is_string($text) && !this.incrementIncludeSize('post-expand', strlen($text))) { +// if (is_string(text) && !this.incrementIncludeSize("post-expand", strlen(text))) { // // Error, oversize inclusion -// if ($titleText !== false) { +// if (titleText !== false) { // // Make a working, properly escaped link if possible (T25588) -// $text = "[[:$titleText]]"; +// text = "[[:titleText]]"; // } else { // // This will probably not be a working link, but at least it may // // provide some hint of where the problem is -// preg_replace('/^:/', '', $originalTitle); -// $text = "[[:$originalTitle]]"; +// preg_replace("/^:/", "", originalTitle); +// text = "[[:originalTitle]]"; // } -// $text .= this.insertStripItem(''); -// this.limitationWarn('post-expand-template-inclusion'); +// text .= this.insertStripItem("", i + 4); + if (XophpInt_.is_false(endPos)) { + // Unclosed comment in input, runs to end + inner = XophpString_.substr(text, i); + accum.Add(XophpArray.New("comment", XophpArray.New(inner))); + i = lengthText; + } else { + // Search backwards for leading whitespace + int wsStart = XophpInt_.is_true(i) ? (i - XophpString_.strspn(revText, strspn_hash__ws_tab, lengthText - i)) : 0; + + // Search forwards for trailing whitespace + // wsEnd will be the position of the last space (or the ">" if there"s none) + int wsEnd = endPos + 2 + XophpString_.strspn(text, strspn_hash__ws_tab, endPos + 3); + + // Keep looking forward as long as we"re finding more + // comments. + XophpArray comments = XophpArray.New(XophpArray.New().Add(wsStart).Add(wsEnd)); + while (XophpString_.eq(XophpString_.substr(text, wsEnd + 1, 4), "c", "a<!--b-->c"); + } + @Test public void Comment__dangling() {// COVERS: "Unclosed comment in input, runs to end" + fxt.Test__parse("a c", "a <!--b--> c"); // NOTE: space is outside comment + } + @Test public void Comment__many__ws() {// COVERS: "Dump all but the last comment to the accumulator" + fxt.Test__parse("a z", "a <!--1--> <!--2--> z"); // NOTE: space is outside comment; + } + @Test public void Comment__nl__ws() { // COVERS: "Eat the line if possible" + fxt.Test__parse(String_.Concat_lines_nl_skip_last + ( "a" + , " " + , " " + , "z" + ), String_.Concat_lines_nl_skip_last + ( "a" + , " <!--1--> " // NOTE: space is inside if flanked by nl; + , " <!--2--> " + , "z" + )); + } + @Test public void Ext() { // COVERS.ALSO: "Note that the attr element contains the whitespace between name and attribute," + fxt.Test__parse("a
b
c", "apre id="1"b</pre>c"); + } + @Test public void Ext__inline() { // COVERS: "if ( $text[$tagEndPos - 1] == '/' ) {" + fxt.Test__parse("a
b"   , "apreb");
+		fxt.Test__parse("a
b"  , "apre b");
+	}
+	@Test  public void Ext__end__pass__space() {// COVERS: "\s*" in `preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",`
+		fxt.Test__parse("a
b
c", "apreb</pre >c"); + } + @Test public void Ext__end__pass__name() { // COVERS: "\s*" in `preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",` + fxt.Test__parse("a
b
c", "apreb</pro></pre>c"); + } + @Test public void Ext__end__fail__angle() {// COVERS: "\s*" in `preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",` + fxt.Test__parse("a
b
a<pre>b</pre c"); + } + @Test public void Ext__dangling() { // COVERS: "Let it run out to the end of the text." + fxt.Test__parse("a
bc", "a<pre>bc");
+	}
+	@Test  public void Ext__dangling__many() {	// COVERS: "Cache results, otherwise we have O(N^2) performance for input like ..."
+		fxt.Test__parse("a
bc", "a<pre><pre><pre>bc");
+	}
+	@Test  public void Ext__unclosed() {		// COVERS: "Infinite backtrack"
+		fxt.Test__parse("a
a<pre bcd");
+	}
+	@Test  public void Ext__noinclude() {	    // COVERS: " and  just become  tags"
+		fxt.Init__for_inclusion_(Bool_.N);
+		fxt.Test__parse("abcde", "a<includeonly>b<noinclude>c</noinclude>d</includeonly>e");
+	}
+	@Test  public void Heading() {
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "== b1 =="
+		, "z"
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "== b1 =="
+		, "z"
+		));
+	}
+	@Test  public void Heading__eos__no_nl() {
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "== b1 =="
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "== b1 =="
+		));
+	}
+	@Test  public void Heading__bos__implied_nl() {  // COVERS: "Is this the start of a heading?"
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "== b1 =="
+		, "z"
+		), String_.Concat_lines_nl_skip_last
+		( "== b1 =="
+		, "z"
+		));
+	}
+	@Test  public void Heading__dwim__y() {	// COVERS: "DWIM: This looks kind of like a name/value separator."
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a{{b|"
+		, "=c="
+		, "}}d"
+		), String_.Concat_lines_nl_skip_last
+		( "ad"
+		));
+	}
+	@Test  public void Heading__dwim__n() {	// COVERS: "DWIM: This looks kind of like a name/value separator."
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a{{b|"
+		, "==c=="
+		, "}}d"
+		), String_.Concat_lines_nl_skip_last
+		( "ad"
+		));
+	}
+	@Test  public void Heading__comment() {	// COVERS: "Comment found at line end"
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "==b== "
+		, ""
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "==b== <!--c-->"
+		, ""
+		));
+	}
+	@Test  public void Heading__consecutive__5() {	// COVERS: "This is just a single String of equals signs on its own line"
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "====="
+		, ""
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "====="
+		, ""
+		));
+	}
+	@Test  public void Heading__consecutive__1() {	// COVERS: "Single equals sign on its own line, count=0"
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "="
+		, ""
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "="
+		, ""
+		));
+	}
+	@Test  public void Heading__unclosed() {	// COVERS: "No match, no , just pass down the inner src"
+		fxt.Test__parse(String_.Concat_lines_nl_skip_last
+		( "a"
+		, "===b"
+		, ""
+		), String_.Concat_lines_nl_skip_last
+		( "a"
+		, "===b"
+		, ""
+		));
+	}
+	@Test  public void Inclusion__n() {
+		fxt.Init__for_inclusion_(Bool_.N);
+		fxt.Test__parse("abc", "a<onlyinclude>b</onlyinclude>c");
+	}
+	@Test  public void Inclusion__y() {
+		fxt.Init__for_inclusion_(Bool_.Y);
+		fxt.Test__parse("abc", "a<onlyinclude>b</onlyinclude>c");
+	}
+	@Test  public void Ignored__noinclude() {	// COVERS: "Handle ignored tags"
+		fxt.Init__for_inclusion_(Bool_.N);
+		fxt.Test__parse("abc", "a<noinclude>b</noinclude>c");
+	}
+}
+class XomwPreprocessor__fxt {
+	private boolean hash_enabled = Bool_.Y;
+	private boolean for_inclusion = false;
+	public XomwPreprocessor__fxt() {
+	}
+	public void Clear() {
+		hash_enabled = true;
+		for_inclusion = false;
+	}
+	public void Init__for_inclusion_(boolean v) {for_inclusion = v;}
+	public XomwPreprocessor__fxt Init__hash_y() {hash_enabled = Bool_.Y; return this;}
+	public void Test__parse(String src_str, String expd) {
+		List_adp list = List_adp_.New();
+		if (hash_enabled) {
+			XomwParser parser = new XomwParser(XomwEnv_fxt.NewTest());
+			XomwPreprocessor_Hash wkr_hash = new XomwPreprocessor_Hash(parser);
+			parser.mStripList.Add("pre");
+			list.Add(wkr_hash);
+		}
+
+		for (int i = 0; i < list.Len(); i++) {
+			XomwPreprocessor wkr = (XomwPreprocessor)list.Get_at(i);
+			String actl = wkr.preprocessToDbg(src_str, for_inclusion);
+			Tfds.Eq_str_lines(expd, actl, src_str);
+		}
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
index 32874b1d2..dabe6faae 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
@@ -4585,7 +4585,7 @@ public class XomwLanguage {
 		// For unknown languages, fallbackSequence returns an empty array,
 		// hardcode fallback to 'en' in that case.
 		Object rv = XomwLanguage.getLocalisationCache().getItem(code, "fallbackSequence");
-		return rv == null ? XophpArray.New().Add("en") : (XophpArray)rv;
+		return rv == null ? XophpArray.New("en") : (XophpArray)rv;
 	}
 
 //		/**
diff --git a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
index beda9ac0b..19b86815b 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
@@ -114,8 +114,8 @@ public class XomwLanguage_tst {
 		fxt.Test_commafy("-1234567890"    , "-1,23,45,67,890");
 	}
 	@Test   public void handleExplicitPluralForms() {
-		fxt.Test__handleExplicitPluralForms__string("1", XophpArray.New().Add("1=one"), "one");
-		fxt.Test__handleExplicitPluralForms__array("1", XophpArray.New().Add("no_match"), XophpArray.New().Add(0, "no_match"));
+		fxt.Test__handleExplicitPluralForms__string("1", XophpArray.New("1=one"), "one");
+		fxt.Test__handleExplicitPluralForms__array("1", XophpArray.New("no_match"), XophpArray.New().Add(0, "no_match"));
 	}
 	@Test   public void getPluralRuleIndexNumber() {
 		fxt.Init__pluralRulesXml
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java
index 05c10e3cb..e49a50662 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java
@@ -36,7 +36,7 @@ class XomwRange {
 		if (end == null) {
 			this.parts.Add(start);
 		} else {
-			this.parts.Add(XophpArray.New().Add(start).Add(end));
+			this.parts.Add(XophpArray.New(start, end));
 		}
 	}
 
diff --git a/baselib/src/gplx/objects/strings/bfrs/String_bfr.java b/baselib/src/gplx/objects/strings/bfrs/String_bfr.java
index f6631e4dd..64a94734b 100644
--- a/baselib/src/gplx/objects/strings/bfrs/String_bfr.java
+++ b/baselib/src/gplx/objects/strings/bfrs/String_bfr.java
@@ -17,6 +17,7 @@ package gplx.objects.strings.bfrs; import gplx.*; import gplx.objects.*; import
 import java.lang.*;
 import gplx.objects.primitives.*;
 import gplx.objects.errs.*;
+import gplx.objects.strings.unicodes.*;
 public class String_bfr {
 	private java.lang.StringBuilder sb = new java.lang.StringBuilder();
 	public boolean Has_none() {return this.Len() == 0;}
@@ -32,6 +33,16 @@ public class String_bfr {
 			Add_char(c);
 		return this;
 	}
+	public String_bfr Add_char_by_code(int code) {
+		if (code >= Ustring_.Surrogate_cp_bgn && code <= Ustring_.Surrogate_cp_end) {
+			sb.append((char)((code - 0x10000) / 0x400 + 0xD800));
+			sb.append((char)((code - 0x10000) % 0x400 + 0xDC00));
+		}
+		else {
+			sb.append((char)code);
+		}
+		return this;
+	}
 	public String_bfr Add_int_pad_bgn(char pad_char, int str_len, int val) {
 		int digit_len   = Int_.Count_digits(val);
 		int pad_len     = str_len - digit_len;