changeset 1716:b82767112d8e

add String.regex
author Franklin Schmidt <fschmidt@gmail.com>
date Sun, 24 Jul 2022 23:43:03 -0600
parents ad44e849c60c
children c637a2a1023d
files conv.txt scripts/test.luan src/luan/LuanJavaFunction.java src/luan/host/https.luan src/luan/host/init.luan src/luan/host/main.luan src/luan/modules/Boot.luan src/luan/modules/Io.luan src/luan/modules/RegexLuan.java src/luan/modules/String.luan src/luan/modules/StringLuan.java src/luan/modules/host/Hosting.luan src/luan/modules/http/Http.luan src/luan/modules/http/Http_test.luan src/luan/modules/http/Server.luan src/luan/modules/http/tools/Run.luan src/luan/modules/lucene/Lucene.luan src/luan/modules/mmake.luan website/src/manual.html.luan
diffstat 19 files changed, 510 insertions(+), 63 deletions(-) [+]
line wrap: on
line diff
--- a/conv.txt	Sat Jul 23 21:53:04 2022 -0600
+++ b/conv.txt	Sun Jul 24 23:43:03 2022 -0600
@@ -1,3 +1,14 @@
+String.find
+String.gmatch
+String.gsub
+String.match
+String.matches
+
+String.contains
+String.starts_with
+String.ends_with
+
+
 rename_to
 
 Mail.Sender
--- a/scripts/test.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/scripts/test.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -17,7 +17,7 @@
 local error = Luan.error
 local range = Luan.range or error()
 local trim = String.trim or error()
-local find = String.find or error()
+local contains = String.contains or error()
 local init = Http_test.init or error()
 local get_page = Http_test.get_page or error()
 local run_page = Http_test.run_page or error()
@@ -38,7 +38,7 @@
 init()
 Http.request.parameters.cmd = "'ab'..'cd'"
 page = run_page(require("luan:http/tools/Shell.luan").respond)
-find(page,"abcd") or error "failed"
+contains(page,"abcd") or error "failed"
 ]]
 
 -- lucene
@@ -84,7 +84,7 @@
 init()
 Http.request.parameters.name = "bob"
 page = get_page "/examples/hi2.html"
-find(page,"bob") or error "failed"
+contains(page,"bob") or error "failed"
 
 
 print "done"
--- a/src/luan/LuanJavaFunction.java	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/LuanJavaFunction.java	Sun Jul 24 23:43:03 2022 -0600
@@ -31,7 +31,6 @@
 
 	private LuanJavaFunction(JavaMethod method,Object obj) {
 		this.method = method;
-		this.obj = obj;
 		this.rtnConverter = getRtnConverter(method);
 		this.takesLuan = takesLuan(method);
 		this.argConverters = getArgConverters(takesLuan,method);
@@ -41,6 +40,16 @@
 		} else {
 			this.varArgCls = null;
 		}
+		this.obj = obj;
+	}
+
+	public LuanJavaFunction(LuanJavaFunction fn,Object obj) {
+		this.method = fn.method;
+		this.rtnConverter = fn.rtnConverter;
+		this.takesLuan = fn.takesLuan;
+		this.argConverters = fn.argConverters;
+		this.varArgCls = fn.varArgCls;
+		this.obj = obj;
 	}
 
 	@Override public String toString() {
--- a/src/luan/host/https.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/host/https.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -8,8 +8,7 @@
 local uri = Io.uri or error()
 local output_of = Io.output_of or error()
 local String = require "luan:String.luan"
-local regex_quote = String.regex_quote or error()
-local matches = String.matches or error()
+local starts_with = String.starts_with or error()
 local Http = require "luan:http/Http.luan"
 local Hosted = require "luan:host/Hosted.luan"
 local Logging = require "luan:logging/Logging.luan"
@@ -148,9 +147,9 @@
 			nginx_file.delete()
 			local_cer_file.delete()
 			local_ca_file.delete()
-			local ptn = [[^]]..regex_quote(domain)..[[\.]]
+			local ptn = domain.."."
 			for _, file in ipairs(dir.children()) do
-				if matches(file.name(),ptn) then
+				if starts_with(file.name(),ptn) then
 					file.delete()
 				end
 			end
--- a/src/luan/host/init.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/host/init.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -2,8 +2,6 @@
 local error = Luan.error
 local do_file = Luan.do_file or error()
 local Package = require "luan:Package.luan"
-local String = require "luan:String.luan"
-local gsub = String.gsub or error()
 local Number = require "luan:Number.luan"
 local long = Number.long or error()
 
--- a/src/luan/host/main.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/host/main.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -7,9 +7,8 @@
 local Rpc = require "luan:Rpc.luan"
 local Thread = require "luan:Thread.luan"
 local String = require "luan:String.luan"
-local regex_quote = String.regex_quote or error()
 local lower = String.lower or error()
-local matches = String.matches or error()
+local starts_with = String.starts_with or error()
 local Hosted = require "luan:host/Hosted.luan"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "main"
@@ -113,7 +112,7 @@
 end
 
 local function security(site_dir,file)
-	matches( file.to_string(), "^"..regex_quote(site_dir.to_string()) ) or error "security violation"
+	starts_with( file.to_string(), site_dir.to_string() ) or error "security violation"
 end
 
 function fns.copy_file(domain,password,dir,name,content)
--- a/src/luan/modules/Boot.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/Boot.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -8,8 +8,8 @@
 local load = BasicLuan.load
 local type = BasicLuan.type
 local StringLuan = require "java:luan.modules.StringLuan"
-local match = StringLuan.match  -- String.match
-local matches = StringLuan.matches  -- String.matches
+local contains = StringLuan.contains  -- String.contains
+local RegexLuan = require "java:luan.modules.RegexLuan"
 local IoLuan = require "java:luan.modules.IoLuan"
 local LuanUrl = require "java:luan.modules.url.LuanUrl"
 local LuanJava = require "java:luan.Luan"
@@ -49,6 +49,21 @@
 Boot.local_metatable = local_metatable
 
 
+local function regex(pattern)
+	local regex = RegexLuan.new(pattern)
+	return {
+		java = regex
+		find = regex.find
+		gmatch = regex.gmatch
+		gsub = regex.gsub
+		match = regex.match
+		matches = regex.matches
+		set = regex.set
+	}
+end
+Boot.regex = regex
+
+
 local function new_LuanIn(io)
 	local this = {}
 	this.java = io
@@ -230,8 +245,10 @@
 Boot.schemes = schemes
 
 
+local uri_regex = regex("(?s)^([^:]+):(.*)$")
+
 local function uri(name,options)
-	local scheme, location = match( name, "(?s)^([^:]+):(.*)$" )
+	local scheme, location = uri_regex.match(name)
 	scheme or error( "invalid Io.uri name '"..name.."', missing scheme" )
 	local opener = schemes[scheme] or error( "invalid scheme '"..scheme.."' in '"..name.."'" )
 	return opener(location,options)
@@ -249,7 +266,7 @@
 
 function Boot.load_file(file)
 	if type(file) == "string" then
-		if not matches(file,":") then
+		if not contains(file,":") then
 			file = "file:"..file
 		end
 		local u = uri(file)
--- a/src/luan/modules/Io.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/Io.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -53,7 +53,6 @@
 local unpack = Table.unpack or error()
 local String = require "luan:String.luan"
 local encode = String.encode or error()
-local matches = String.matches or error()
 
 
 -- do not change
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/RegexLuan.java	Sun Jul 24 23:43:03 2022 -0600
@@ -0,0 +1,147 @@
+package luan.modules;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import luan.Luan;
+import luan.LuanMutable;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+
+
+public final class RegexLuan implements LuanMutable {
+	public Pattern pattern;
+	private boolean immutable = false;
+
+	public RegexLuan(String s) {
+		this.pattern = Pattern.compile(s);
+	}
+
+	@Override public boolean isImmutable() {
+		return immutable;
+	}
+
+	@Override public void makeImmutable() {
+		if(immutable)
+			return;
+		immutable = true;
+	}
+
+	public void set(String s) throws LuanException {
+		if( immutable )
+			throw new LuanException("regex is immutable");
+		this.pattern = Pattern.compile(s);
+	}
+
+	public Object[] find(String s,Integer init) throws LuanException {
+		int start = StringLuan.start(s,init,0);
+		Matcher m = pattern.matcher(s);
+		if( !m.find(start) )
+			return null;
+		int n = m.groupCount();
+		Object[] rtn = new Object[2+n];
+		rtn[0] = m.start() + 1;
+		rtn[1] = m.end();
+		for( int i=0; i<n; i++ ) {
+			rtn[2+i] = m.group(i+1);
+		}
+		return rtn;
+	}
+
+	public LuanFunction gmatch(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		final Matcher m = pattern.matcher(s);
+		return new LuanFunction() {
+			@Override public Object call(Luan luan,Object[] args) {
+				if( !m.find() )
+					return null;
+				final int n = m.groupCount();
+				if( n == 0 )
+					return m.group();
+				String[] rtn = new String[n];
+				for( int i=0; i<n; i++ ) {
+					rtn[i] = m.group(i+1);
+				}
+				return rtn;
+			}
+		};
+	}
+
+	public Object[] gsub(Luan luan,String s,Object repl,Integer n) throws LuanException {
+		Utils.checkNotNull(s);
+		int max = n==null ? Integer.MAX_VALUE : n;
+		final Matcher m = pattern.matcher(s);
+		if( repl instanceof String ) {
+			String replacement = (String)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				m.appendReplacement(sb,replacement);
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		if( repl instanceof LuanTable ) {
+			LuanTable t = (LuanTable)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				String match = m.groupCount()==0 ? m.group() : m.group(1);
+				Object val = t.get(luan,match);
+				if( val != null ) {
+					String replacement = luan.luanToString(val);
+					m.appendReplacement(sb,replacement);
+				}
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		if( repl instanceof LuanFunction ) {
+			LuanFunction fn = (LuanFunction)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				Object[] args;
+				final int count = m.groupCount();
+				if( count == 0 ) {
+					args = new String[]{m.group()};
+				} else {
+					args = new String[count];
+					for( int j=0; j<count; j++ ) {
+						args[j] = m.group(j+1);
+					}
+				}
+				Object val = Luan.first( fn.call(luan,args) );
+				if( val != null ) {
+					String replacement = luan.luanToString(val);
+					m.appendReplacement(sb,replacement);
+				}
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		throw new LuanException( "bad argument #3 to 'gsub' (string/function/table expected)" );
+	}
+
+	public String[] match(String s,Integer init) throws LuanException {
+		int start = StringLuan.start(s,init,0);
+		Matcher m = pattern.matcher(s);
+		if( !m.find(start) )
+			return null;
+		int n = m.groupCount();
+		if( n == 0 )
+			return new String[]{m.group()};
+		String[] rtn = new String[n];
+		for( int i=0; i<n; i++ ) {
+			rtn[i] = m.group(i+1);
+		}
+		return rtn;
+	}
+
+	public boolean matches(String s) {
+		return pattern.matcher(s).find();
+	}
+}
--- a/src/luan/modules/String.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/String.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -1,12 +1,15 @@
 require "java"
 local StringLuan = require "java:luan.modules.StringLuan"
 local Pattern = require "java:java.util.regex.Pattern"
+local Boot = require "luan:Boot.luan"
 
 local String = {}
 
 String.char = StringLuan.char_
+String.contains = StringLuan.contains
 String.digest_message = StringLuan.digest_message
 String.encode = StringLuan.encode
+String.ends_with = StringLuan.ends_with
 String.find = StringLuan.find
 String.format = StringLuan.format
 String.gmatch = StringLuan.gmatch
@@ -14,11 +17,12 @@
 String.lower = StringLuan.lower
 String.match = StringLuan.match
 String.matches = StringLuan.matches
-String.regex_compile = Pattern.compile
+String.regex = Boot.regex
 String.regex_quote = Pattern.quote
 String.rep = StringLuan.rep
 String.reverse = StringLuan.reverse
 String.split = StringLuan.split
+String.starts_with = StringLuan.starts_with
 String.sub = StringLuan.sub
 String.to_binary = StringLuan.to_binary
 String.to_number = StringLuan.to_number
--- a/src/luan/modules/StringLuan.java	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/StringLuan.java	Sun Jul 24 23:43:03 2022 -0600
@@ -92,14 +92,13 @@
 		return s.substring(start,end);
 	}
 
-	public static Object[] find(String s,Object pattern,Integer init,Boolean plain) throws LuanException {
+	public static Object[] find(String s,String pattern,Integer init,Boolean plain) {
 		int start = start(s,init,0);
 		if( Boolean.TRUE.equals(plain) ) {
-			String ptn = (String)pattern;
-			int i = s.indexOf(ptn,start);
-			return i == -1 ? null : new Integer[]{i+1,i+ptn.length()};
+			int i = s.indexOf(pattern,start);
+			return i == -1 ? null : new Integer[]{i+1,i+pattern.length()};
 		}
-		Matcher m = pattern(pattern).matcher(s);
+		Matcher m = Pattern.compile(pattern).matcher(s);
 		if( !m.find(start) )
 			return null;
 		int n = m.groupCount();
@@ -112,9 +111,9 @@
 		return rtn;
 	}
 
-	public static String[] match(String s,Object pattern,Integer init) throws LuanException {
+	public static String[] match(String s,String pattern,Integer init) {
 		int start = start(s,init,0);
-		Matcher m = pattern(pattern).matcher(s);
+		Matcher m = Pattern.compile(pattern).matcher(s);
 		if( !m.find(start) )
 			return null;
 		int n = m.groupCount();
@@ -127,9 +126,10 @@
 		return rtn;
 	}
 
-	public static LuanFunction gmatch(String s,Object pattern) throws LuanException {
+	public static LuanFunction gmatch(String s,String pattern) throws LuanException {
 		Utils.checkNotNull(s);
-		final Matcher m = pattern(pattern).matcher(s);
+		Utils.checkNotNull(pattern,2);
+		final Matcher m = Pattern.compile(pattern).matcher(s);
 		return new LuanFunction() {
 			@Override public Object call(Luan luan,Object[] args) {
 				if( !m.find() )
@@ -146,10 +146,10 @@
 		};
 	}
 
-	public static Object[] gsub(Luan luan,String s,Object pattern,Object repl,Integer n) throws LuanException {
+	public static Object[] gsub(Luan luan,String s,String pattern,Object repl,Integer n) throws LuanException {
 		Utils.checkNotNull(s);
 		int max = n==null ? Integer.MAX_VALUE : n;
-		final Matcher m = pattern(pattern).matcher(s);
+		final Matcher m = Pattern.compile(pattern).matcher(s);
 		if( repl instanceof String ) {
 			String replacement = (String)repl;
 			int i = 0;
@@ -226,24 +226,14 @@
 		return null;
 	}
 
-	private static Pattern pattern(Object pattern) throws LuanException {
-		if( pattern instanceof Pattern ) {
-			return (Pattern)pattern;
-		} else if( pattern instanceof String ) {
-			return Pattern.compile((String)pattern);
-		} else {
-			throw new LuanException( "bad argument #2 (string or compiled pattern expected)" );
-		}
-	}
-
-	public static boolean matches(String s,Object pattern) throws LuanException {
+	public static boolean matches(String s,String pattern) throws LuanException {
 		Utils.checkNotNull(s);
-		return pattern(pattern).matcher(s).find();
+		return Pattern.compile(pattern).matcher(s).find();
 	}
 
 	public static String[] split(String s,String pattern,Integer limit) throws LuanException {
 		Utils.checkNotNull(s);
-		Utils.checkNotNull(pattern,1);
+		Utils.checkNotNull(pattern,2);
 		int n = limit==null ? -1 : limit;
 		return s.split(pattern,n);
 	}
@@ -254,4 +244,22 @@
 		return BinaryLuan.to_hex( BinaryLuan.digest_message( algorithm, input.getBytes() ) );
 	}
 
+	public static boolean contains(String s,String s2) throws LuanException {
+		Utils.checkNotNull(s);
+		Utils.checkNotNull(s2,2);
+		return s.contains(s2);
+	}
+
+	public static boolean starts_with(String s,String s2) throws LuanException {
+		Utils.checkNotNull(s);
+		Utils.checkNotNull(s2,2);
+		return s.startsWith(s2);
+	}
+
+	public static boolean ends_with(String s,String s2) throws LuanException {
+		Utils.checkNotNull(s);
+		Utils.checkNotNull(s2,2);
+		return s.endsWith(s2);
+	}
+
 }
--- a/src/luan/modules/host/Hosting.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/host/Hosting.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -8,7 +8,8 @@
 local print = Io.print or error()
 local Rpc = require "luan:Rpc.luan"
 local String = require "luan:String.luan"
-local matches = String.matches or error()
+local contains = String.contains or error()
+local starts_with = String.starts_with or error()
 local substring = String.sub or error()
 local split = String.split or error()
 local Logging = require "luan:logging/Logging.luan"
@@ -43,7 +44,7 @@
 			end
 			for _, here_child in ipairs(here.children()) do
 				local name = here_child.name()
-				if not matches(name,[[^\.]]) then
+				if not starts_with(name,".") then
 					process(there,there.children[name],here_child)
 					there.children[name] = nil
 				end
@@ -73,7 +74,7 @@
 	my_file.is_file() or error("'"..file.."' is not a file")
 	local my_file_string = my_file.to_string()
 	local my_dir_string = my_dir.to_string().."/"
-	matches( my_file_string, [[^\Q]]..my_dir_string..[[\E]] ) or error "file must be in dir"
+	contains( my_file_string, my_dir_string ) or error "file must be in dir"
 	my_file_string = substring(my_file_string,#my_dir_string+1)
 	local path = {split( my_file_string, "/" )}
 	path[#path] = nil
--- a/src/luan/modules/http/Http.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/http/Http.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -19,7 +19,6 @@
 local Package = require "luan:Package.luan"
 local String = require "luan:String.luan"
 local lower = String.lower or error()
-local matches = String.matches or error()
 local trim = String.trim or error()
 local Time = require "luan:Time.luan"
 local time_format = Time.format or error()
--- a/src/luan/modules/http/Http_test.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/http/Http_test.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -4,7 +4,7 @@
 local Package = require "luan:Package.luan"
 local Io = require "luan:Io.luan"
 local String = require "luan:String.luan"
-local matches = String.matches or error()
+local ends_with = String.ends_with or error()
 local Http = require "luan:http/Http.luan"
 
 
@@ -15,7 +15,7 @@
 
 function Http_test.get_page(path)
 	Http.request.path = path
-	if Http_test.welcome_file ~= nil and matches(path,"/$") then
+	if Http_test.welcome_file ~= nil and ends_with(path,"/") then
 		path = path .. Http_test.welcome_file
 	end
 	local old_out = Io.stdout
--- a/src/luan/modules/http/Server.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/http/Server.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -1,9 +1,8 @@
 local Luan = require "luan:Luan.luan"
 local error = Luan.error
 local String = require "luan:String.luan"
-local gsub = String.gsub or error()
-local match = String.match or error()
-local matches = String.matches or error()
+local regex = String.regex or error()
+local contains = String.contains or error()
 local Io = require "luan:Io.luan"
 local uri = Io.uri or error()
 local Package = require "luan:Package.luan"
@@ -31,10 +30,10 @@
 local Server = {}
 
 function Server.init_dir(dir)
-	if not matches(dir,":") then
+	if not contains(dir,":") then
 		dir = "file:"..dir
 	end
-	dir = gsub(dir,"/$","")  -- remove trailing '/' if any
+	dir = regex("/$").gsub(dir,"")  -- remove trailing '/' if any
 	Http.dir = dir
 	Http.is_serving = true
 	function Io.schemes.site(path)
@@ -63,10 +62,12 @@
 	Thread.fork(Rpc.serve)
 end
 
+local file_regex = regex("^file:(.*)$")
+
 function Server.serve(dir,port)
 	port = port or 8080
 	Server.init_dir(dir)
-	local dir_path = match(Http.dir,"^file:(.*)$") or error "server dir must be scheme 'file:'"
+	local dir_path = file_regex.match(Http.dir) or error "server dir must be scheme 'file:'"
 	local file_handler = FileHandler.new(dir_path)
 	local luan_handler = LuanHandler.new()
 	local handler = ListHandler.new( luan_handler, file_handler )
--- a/src/luan/modules/http/tools/Run.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/http/tools/Run.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -4,14 +4,16 @@
 local Io = require "luan:Io.luan"
 local print = Io.print or error()
 local String = require "luan:String.luan"
-local gmatch = String.gmatch or error()
+local regex = String.regex or error()
 local Http = require "luan:http/Http.luan"
 
 
 local Run = {}
 
+local line_regex = regex("([^\n]*)\n|([^\n]+)$")
+
 local function lines(s)
-	local matcher = gmatch(s,"([^\n]*)\n|([^\n]+)$")
+	local matcher = line_regex.gmatch(s)
 	return function()
 		local m1, m2 = matcher()
 		return m1 or m2
--- a/src/luan/modules/lucene/Lucene.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/lucene/Lucene.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -15,7 +15,7 @@
 local Io = require "luan:Io.luan"
 local uri = Io.uri or error()
 local String = require "luan:String.luan"
-local matches = String.matches or error()
+local starts_with = String.starts_with or error()
 local Rpc = require "luan:Rpc.luan"
 local LuceneIndex = require "java:luan.modules.lucene.LuceneIndex"
 local NumberFieldParser = require "java:goodjava.lucene.queryparser.NumberFieldParser"
@@ -64,7 +64,7 @@
 
 local function get_file(f)
 	type(f)=="table" or error "index_dir must be table"
-	f.to_uri_string and matches(f.to_uri_string(),"^file:") or error "index_dir must be file"
+	f.to_uri_string and starts_with(f.to_uri_string(),"file:") or error "index_dir must be file"
 	return f.java.file or error()
 end
 
--- a/src/luan/modules/mmake.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/src/luan/modules/mmake.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -5,7 +5,12 @@
 local print = Io.print
 local output_to = Io.output_to
 local String = require "luan:String.luan"
+local regex = String.regex
+local substring = String.sub
+local ends_with = String.ends_with
 local Time = require "luan:Time.luan"
+local time_now = Time.now
+local time_format = Time.format
 
 
 local compiler = Table.concat( { "javac -g -encoding UTF8", ... }, " " )
@@ -13,7 +18,7 @@
 
 local function header()
 %>
-# Makefile created on <%=Time.format(Time.now())%> by Mmake
+# Makefile created on <%=time_format(time_now())%> by Mmake
 
 .SUFFIXES: .java .class
 
@@ -29,8 +34,8 @@
 	local dirs = {}
 	for _, file in ipairs(dir.children()) do
 		local name = file.name()
-		if String.matches(name,[[\.java$]]) then
-			javas[#javas+1] = String.sub(name,1,-6)
+		if ends_with(name,".java") then
+			javas[#javas+1] = substring(name,1,-6)
 		end
 		if file.is_directory() and mmake(file) then
 			dirs[#dirs+1] = name
@@ -41,8 +46,9 @@
 	end
 	local out = dir.child("Makefile").text_writer()
 	output_to(out,header)
+	local r = regex([[\$]])
 	for _, s in ipairs(javas) do
-		s = String.gsub(s,[[\$]],[[\$\$]])
+		s = r.gsub(s,[[\$\$]])
 		out.write( "\\\n\t\t",  s , ".class" )
 	end
 	for _, s in ipairs(dirs) do
--- a/website/src/manual.html.luan	Sat Jul 23 21:53:04 2022 -0600
+++ b/website/src/manual.html.luan	Sun Jul 24 23:43:03 2022 -0600
@@ -2415,6 +2415,16 @@
 <%
 						end
 					}
+					["String.contains"] = {
+						title = "<code>String.contains (s, s2)</code>"
+						content = function()
+%>
+<p>
+Returns a boolean indicating whether the <code>s</code> contains <code>s2</code>.
+</p>
+<%
+						end
+					}
 					["String.encode"] = {
 						title = "<code>String.encode (s)</code>"
 						content = function()
@@ -2425,6 +2435,16 @@
 <%
 						end
 					}
+					["String.ends_with"] = {
+						title = "<code>String.ends_with (s, s2)</code>"
+						content = function()
+%>
+<p>
+Returns a boolean indicating whether the <code>s</code> ends with <code>s2</code>.
+</p>
+<%
+						end
+					}
 					["String.find"] = {
 						title = "<code>String.find (s, pattern [, init [, plain]])</code>"
 						content = function()
@@ -2634,6 +2654,16 @@
 <%
 						end
 					}
+					["String.regex"] = {
+						title = "<code>String.regex (s)</code>"
+						content = function()
+%>
+<p>
+Returns a <a href="#regex_table">regex</a> table for the pattern <code>s</code>.
+</p>
+<%
+						end
+					}
 					["String.regex_quote"] = {
 						title = "<code>String.regex_quote (s)</code>"
 						content = function()
@@ -2678,6 +2708,16 @@
 <%
 						end
 					}
+					["String.starts_with"] = {
+						title = "<code>String.starts_with (s, s2)</code>"
+						content = function()
+%>
+<p>
+Returns a boolean indicating whether the <code>s</code> starts with <code>s2</code>.
+</p>
+<%
+						end
+					}
 					["String.sub"] = {
 						title = "<code>String.sub (s, i [, j])</code>"
 						content = function()
@@ -2785,6 +2825,205 @@
 					}
 				}
 			}
+			regex_table = {
+				title = "Regular Expressions"
+				content = function()
+%>
+<p>
+Regular expressions are handled using a regex table generated by <a href="#String.regex">String.regex</a>.
+</p>
+
+<p>
+Pattern matching is based on the Java <a href="http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">Pattern</a> class.
+</p>
+<%
+				end
+				subs = {
+					["regex.find"] = {
+						title = "<code>regex.find (s [, init])</code>"
+						content = function()
+%>
+<p>
+Looks for the first match of
+the regex in the string <code>s</code>.
+If it finds a match, then <code>find</code> returns the indices of&nbsp;<code>s</code>
+where this occurrence starts and ends;
+otherwise, it returns <b>nil</b>.
+A third, optional numerical argument <code>init</code> specifies
+where to start the search;
+its default value is&nbsp;1 and can be negative.
+</p>
+
+<p>
+If the regex has captures,
+then in a successful match
+the captured values are also returned,
+after the two indices.
+</p>
+<%
+						end
+					}
+					["regex.gmatch"] = {
+						title = "<code>regex.gmatch (s)</code>"
+						content = function()
+%>
+<p>
+Returns an iterator function that,
+each time it is called,
+returns the next captures from the regex
+over the string <code>s</code>.
+If the regex specifies no captures,
+then the whole match is produced in each call.
+</p>
+
+<p>
+As an example, the following loop
+will iterate over all the words from string <code>s</code>,
+printing one per line:
+</p>
+<pre>
+	local r = String.regex[[\w+]]
+	local s = "hello world from Lua"
+	for w in r.gmatch(s) do
+		print(w)
+	end
+</pre>
+
+<p>
+The next example collects all pairs <code>key=value</code> from the
+given string into a table:
+</p>
+<pre>
+	local t = {}
+	local r = String.regex[[(\w+)=(\w+)]]
+	local s = "from=world, to=Lua"
+	for k, v in r.gmatch(s) do
+		t[k] = v
+	end
+</pre>
+
+<p>
+For this function, a caret '<code>^</code>' at the start of a pattern does not
+work as an anchor, as this would prevent the iteration.
+</p>
+<%
+						end
+					}
+					["regex.gsub"] = {
+						title = "<code>regex.gsub (s, repl [, n])</code>"
+						content = function()
+%>
+<p>
+Returns a copy of <code>s</code>
+in which all (or the first <code>n</code>, if given)
+occurrences of the regex have been
+replaced by a replacement string specified by <code>repl</code>,
+which can be a string, a table, or a function.
+<code>gsub</code> also returns, as its second value,
+the total number of matches that occurred.
+The name <code>gsub</code> comes from <em>Global SUBstitution</em>.
+</p>
+
+<p>
+If <code>repl</code> is a string, then its value is used for replacement.
+The character&nbsp;<code>\</code> works as an escape character.
+Any sequence in <code>repl</code> of the form <code>$<em>d</em></code>,
+with <em>d</em> between 1 and 9,
+stands for the value of the <em>d</em>-th captured substring.
+The sequence <code>$0</code> stands for the whole match.
+</p>
+
+<p>
+If <code>repl</code> is a table, then the table is queried for every match,
+using the first capture as the key.
+</p>
+
+<p>
+If <code>repl</code> is a function, then this function is called every time a
+match occurs, with all captured substrings passed as arguments,
+in order.
+</p>
+
+<p>
+In any case,
+if the regex specifies no captures,
+then it behaves as if the whole regex was inside a capture.
+</p>
+
+<p>
+If the value returned by the table query or by the function call
+is not <b>nil</b>,
+then it is used as the replacement string;
+otherwise, if it is <b>nil</b>,
+then there is no replacement
+(that is, the original match is kept in the string).
+</p>
+
+<p>
+Here are some examples:
+</p>
+<pre>
+	local r = String.regex[[(\w+)]]
+	local x = r.gsub("hello world", "$1 $1")
+	--&gt; x="hello hello world world"
+
+	local r = String.regex[[(\w+)]]
+	local x = r.gsub("hello world", "$0 $0", 1)
+	--&gt; x="hello hello world"
+
+	local r = String.regex[[(\w+)\s*(\w+)]]
+	local x = r.gsub("hello world from Luan", "$2 $1")
+	--&gt; x="world hello Luan from"
+	     
+	local r = String.regex[[\$(.*?)\$]]
+	local x = r.gsub("4+5 = $return 4+5$", function(s)
+		return load(s)()
+	end)
+	--&gt; x="4+5 = 9"
+
+	local r = String.regex[[\$(\w+)]]
+	local t = {name="lua", version="5.3"}
+	local x = r.gsub("$name-$version.tar.gz", t)
+	--&gt; x="lua-5.3.tar.gz"
+</pre>
+<%
+						end
+					}
+					["regex.match"] = {
+						title = "<code>regex.match (s [, init])</code>"
+						content = function()
+%>
+<p>
+Looks for the first <em>match</em> of
+the regex in the string <code>s</code>.
+If it finds one, then <code>match</code> returns
+the captures from the regex;
+otherwise it returns <b>nil</b>.
+If the regex specifies no captures,
+then the whole match is returned.
+A third, optional numerical argument <code>init</code> specifies
+where to start the search;
+its default value is&nbsp;1 and can be negative.
+</p>
+<%
+						end
+					}
+					["regex.matches"] = {
+						title = "<code>regex.matches (s)</code>"
+						content = function()
+%>
+<p>
+Returns a boolean indicating whether the regex can be found in string <code>s</code>.
+This function is equivalent to
+</p>
+<pre>
+     return regex.match(s) ~= nil
+</pre>
+<%
+						end
+					}
+				}
+			}
 			binary_lib = {
 				title = "Binary Manipulation"
 				content = function()
@@ -3375,6 +3614,14 @@
 				display: inline-block;
 				width: 100px;
 			}
+			code {
+				font-size: 16px;
+				font-weight: bold;
+			}
+			div[toc] code {
+				font-size: inherit;
+				font-weight: inherit;
+			}
 		</style>
 	</head>
 	<body>