view src/goodjava/bbcode/BBCode.java @ 1712:36c28be6d432

improve html and bbcode
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 14 Jul 2022 22:14:21 -0600
parents 05d14db623b6
children 31a82b0d0a87
line wrap: on
line source

package goodjava.bbcode;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import goodjava.parser.Parser;


public final class BBCode {

	private static final Pattern tagPtn = Pattern.compile(
		"\\[(/?[a-zA-Z]+(=[^ \\n\\t\\[\\]]*)?)\\]"
	);

	public static String encode(String s) {
		return tagPtn.matcher(s).replaceAll("[brackets]$1[/brackets]");
	}


	public final class Element {
		public final String name;
		public final String param;
		public final Object contents;  // String, Element, or List
		public final Map<String,String> extra;

		private Element(String name,Object contents) {
			this(name,null,contents);
		}

		private Element(String name,String param,Object contents) {
			this(name,param,contents,null);
		}

		private Element(String name,String param,Object contents,Map<String,String> extra) {
			this.name = name.toLowerCase();
			this.param = param;
			this.contents = contents;
			this.extra = extra;
		}
	}

	public static Object parse(String text) {
		if( text.indexOf('[') == -1 )
			return text;
		return new BBCode(text).parse();
	}

	private final Parser parser;

	private BBCode(String text) {
		this.parser = new Parser(text);
	}

	private Object parse() {
		List list = new ArrayList();
		StringBuilder text = new StringBuilder();
		while( !parser.endOfInput() ) {
			Element block = parseBlock(false);
			if( block != null ) {
				add(list,text);
				list.add( block );
			} else {
				text.append( parser.currentChar() );
				parser.anyChar();
			}
		}
		add(list,text);
		return list.size()==1 ? list.get(0) : list;
	}

	private Object parseUntil(String end) {
		return parseUntil(end,false);
	}

	private Object parseUntil(String end,boolean inList) {
		List list = new ArrayList();
		StringBuilder text = new StringBuilder();
		while( !parser.matchIgnoreCase(end) ) {
			if( parser.endOfInput() )
				return null;
			Element block = parseBlock(inList);
			if( block != null ) {
				add(list,text);
				list.add( block );
			} else {
				text.append( parser.currentChar() );
				parser.anyChar();
			}
		}
		add(list,text);
		return list.size()==1 ? list.get(0) : list;
	}

	private void add(List list,StringBuilder text) {
		if( text.length() > 0 ) {
			list.add( text.toString() );
			text.setLength(0);
		}
	}

	private Element parseBlock(boolean inList) {
		if( parser.currentChar() != '[' )
			return null;
		Element s;
		s = parseB();  if(s!=null) return s;
		s = parseI();  if(s!=null) return s;
		s = parseU();  if(s!=null) return s;
		s = parseS();  if(s!=null) return s;
		s = parseSup();  if(s!=null) return s;
		s = parseBrackets();  if(s!=null) return s;
		s = parseUrl();  if(s!=null) return s;
		s = parseCode();  if(s!=null) return s;
		s = parseImg();  if(s!=null) return s;
		s = parseColor();  if(s!=null) return s;
		s = parseSize();  if(s!=null) return s;
		s = parseVideo();  if(s!=null) return s;
		s = parseQuote();  if(s!=null) return s;
		s = parseUl();  if(s!=null) return s;
		s = parseOl();  if(s!=null) return s;
		if( inList ) {
			s = parseLi();  if(s!=null) return s;
		}
		return null;
	}

	private Element parseB() {
		parser.begin();
		if( !parser.matchIgnoreCase("[b]") )
			return parser.failure(null);
		Object content = parseUntil("[/b]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("b",content);
		return parser.success(rtn);
	}

	private Element parseI() {
		parser.begin();
		if( !parser.matchIgnoreCase("[i]") )
			return parser.failure(null);
		Object content = parseUntil("[/i]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("i",content);
		return parser.success(rtn);
	}

	private Element parseU() {
		parser.begin();
		if( !parser.matchIgnoreCase("[u]") )
			return parser.failure(null);
		Object content = parseUntil("[/u]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("u",content);
		return parser.success(rtn);
	}

	private Element parseS() {
		parser.begin();
		if( !parser.matchIgnoreCase("[s]") )
			return parser.failure(null);
		Object content = parseUntil("[/s]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("s",content);
		return parser.success(rtn);
	}

	private Element parseSup() {
		parser.begin();
		if( !parser.matchIgnoreCase("[sup]") )
			return parser.failure(null);
		Object content = parseUntil("[/sup]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("sup",content);
		return parser.success(rtn);
	}

	private Element parseBrackets() {
		parser.begin();
		if( !parser.matchIgnoreCase("[brackets]") )
			return parser.failure(null);
		Object content = parseUntil("[/brackets]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("brackets",content);
		return parser.success(rtn);
	}

	private Element parseUrl() {
		parser.begin();
		if( !parser.matchIgnoreCase("[url") )
			return parser.failure(null);
		String url;
		Object content;
		if( parser.match(']') ) {
			url = parseRealUrl();
			content = null;
			if( !parser.matchIgnoreCase("[/url]") )
				return parser.failure(null);
		} else if( parser.match('=') ) {
			url = parseRealUrl();
			if( !parser.match(']') )
				return parser.failure(null);
			content = parseUntil("[/url]");
			if( content==null )
				return parser.failure(null);
		} else
			return parser.failure(null);
		Element rtn = new Element("url",url,content);
		return parser.success(rtn);
	}

	private String parseRealUrl() {
		parser.begin();
		while( parser.match(' ') );
		int start = parser.currentIndex();
		if( !parser.matchIgnoreCase("http") )
			return parser.failure(null);
		parser.matchIgnoreCase("s");
		if( !parser.matchIgnoreCase("://") )
			return parser.failure(null);
		while( parser.noneOf(" \n\t[]") );
		String url = parser.textFrom(start);
		while( parser.match(' ') );
		return parser.success(url);
	}

	private Element parseCode() {
		parser.begin();
		String param;
		String end;
		if( !parser.matchIgnoreCase("[code") )
			return parser.failure(null);
		if( parser.match(']') ) {
			param = null;
			end = "[/code]";
		} else if( parser.match('=') ) {
			int start = parser.currentIndex();
			while( parser.noneOf("[]\n") );
			param = parser.textFrom(start);
			if( !parser.match(']') )
				return parser.failure(null);
			end = "[/code=" + param + "]";
		} else
			return parser.failure(null);
		int start = parser.currentIndex();
		while( !parser.testIgnoreCase(end) ) {
			if( !parser.anyChar() )
				return parser.failure(null);
		}
		String content = parser.textFrom(start);
		if( !parser.matchIgnoreCase(end) ) throw new RuntimeException();
		Element rtn = new Element("code",param,content);
		return parser.success(rtn);
	}

	private Element parseImg() {
		parser.begin();
		if( !parser.matchIgnoreCase("[img]") )
			return parser.failure(null);
		String url = parseRealUrl();
		if( !parser.matchIgnoreCase("[/img]") )
			return parser.failure(null);
		Element rtn = new Element("img",url);
		return parser.success(rtn);
	}

	private Element parseColor() {
		parser.begin();
		if( !parser.matchIgnoreCase("[color=") )
			return parser.failure(null);
		int start = parser.currentIndex();
		parser.match('#');
		while( parser.inCharRange('0','9')
			|| parser.inCharRange('a','z')
			|| parser.inCharRange('A','Z')
		);
		String color = parser.textFrom(start);
		if( !parser.match(']') )
			return parser.failure(null);
		Object content = parseUntil("[/color]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("color",color,content);
		return parser.success(rtn);
	}

	private Element parseSize() {
		parser.begin();
		if( !parser.matchIgnoreCase("[size=") )
			return parser.failure(null);
		int start = parser.currentIndex();
		while( parser.match('.') || parser.inCharRange('0','9') );
		String size = parser.textFrom(start);
		if( !parser.match(']') )
			return parser.failure(null);
		Object content = parseUntil("[/size]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("size",size,content);
		return parser.success(rtn);
	}

	private static final Pattern youtubePtn1 = Pattern.compile("https://youtu.be/([a-zA-Z0-9_-]+)(?:\\?t=([0-9]+))?");
	private static final Pattern youtubePtn2 = Pattern.compile("https://www.youtube.com/watch?v=([a-zA-Z0-9_-]+)(?:\\?t=([0-9]+)s)?");
	private static final Pattern bitchutePtn = Pattern.compile("https://www.bitchute.com/video/([a-zA-Z0-9]+)/");

	private static Matcher find(Pattern ptn,String s) {
		Matcher m = ptn.matcher(s);
		return m.find() ? m : null;
	}

	private Element parseVideo() {
		parser.begin();
		if( !parser.matchIgnoreCase("[video]") )
			return parser.failure(null);
		String url = parseRealUrl();
		if( !parser.matchIgnoreCase("[/video]") )
			return parser.failure(null);
		Map<String,String> extra = new LinkedHashMap<String,String>();
		Element rtn = new Element("video",null,url,extra);
		Matcher m;
		m = find(youtubePtn1,url);
		if( m == null )
			m = find(youtubePtn2,url);
		if( m != null ) {
			extra.put( "site", "youtube" );
			extra.put( "id", m.group(1) );
			String t = m.group(2);
			if( t != null )
				extra.put( "start", t );
			return parser.success(rtn);
		}
		m = find(bitchutePtn,url);
		if( m != null ) {
			extra.put( "site", "bitchute" );
			extra.put( "id", m.group(1) );
			return parser.success(rtn);
		}
		return parser.success(rtn);
	}


	private Element parseQuote() {
		parser.begin();
		if( !parser.matchIgnoreCase("[quote") )
			return parser.failure(null);
		String name;
		if( parser.match(']') ) {
			name = null;
		} else if( parser.match('=') ) {
			int start = parser.currentIndex();
			while( parser.noneOf("[]\n") );
			name = parser.textFrom(start);
			if( !parser.match(']') )
				return parser.failure(null);
		} else
			return parser.failure(null);
		Object content = parseUntil("[/quote]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("quote",name,content);
		return parser.success(rtn);
	}


	private Element parseUl() {
		parser.begin();
		if( !parser.matchIgnoreCase("[ul]") )
			return parser.failure(null);
		Object content = parseUntil("[/ul]",true);
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("ul",content);
		return parser.success(rtn);
	}

	private Element parseOl() {
		parser.begin();
		if( !parser.matchIgnoreCase("[ol]") )
			return parser.failure(null);
		Object content = parseUntil("[/ol]",true);
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("ol",content);
		return parser.success(rtn);
	}

	private Element parseLi() {
		parser.begin();
		if( !parser.matchIgnoreCase("[li]") )
			return parser.failure(null);
		Object content = parseUntil("[/li]");
		if( content==null )
			return parser.failure(null);
		Element rtn = new Element("li",content);
		return parser.success(rtn);
	}

}