view src/nabble/naml/compiler/Macro.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children
line wrap: on
line source

package nabble.naml.compiler;

import fschmidt.util.java.CollectionUtils;
import nabble.naml.dom.Attribute;
import nabble.naml.dom.Cdata;
import nabble.naml.dom.Container;
import nabble.naml.dom.Element;
import nabble.naml.dom.ElementName;
import nabble.naml.dom.EmptyElement;
import nabble.naml.dom.Naml;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;


public final class Macro implements Call, Meaning {
	private static final Logger logger = LoggerFactory.getLogger(Macro.class);

	public enum Type {
		MACRO, SUBROUTINE, TRANSLATION, NAMESPACE;

		final String tagName;

		Type() {
			this.tagName = name().toLowerCase();
		}
	};

	static final String OVERRIDE_PREFIX = "override_";

	public final Source source;
	public final Element element;
	final String name;
	final Set<String> parameters = new HashSet<String>();
	final String dotParam;
	final List<String> requiredNamespaces;
	final boolean isOverride;
	final Naml naml;
	final Type type;
	final String extendsNs;
	private final String id;

	private static final ElementName fromName = new ElementName("from");
	private static final ElementName toName = new ElementName("to");
	private static final ElementName doName = new ElementName("n.do");

	Macro(Source source,Element element,StackTrace stackTrace,Map<String,Integer> macroCount) throws CompileException {
		this.source = source;
		this.element = element;
		Map<String,Attribute> attrs = element.attributeMap();
		String tagName = element.name().parts().get(0).text();
		if( tagName.startsWith(OVERRIDE_PREFIX) ) {
			this.isOverride = true;
			tagName = tagName.substring(OVERRIDE_PREFIX.length());
		} else {
			this.isOverride = false;
		}
		if( tagName.equals(Type.NAMESPACE.tagName) ) {
			this.type = Type.NAMESPACE;
			if( !(element instanceof EmptyElement) )
				throw new CompileException(stackTrace,element.name()+" must be empty");
			this.name = asString(attrs.remove("name"),stackTrace);
			if( name==null )
				throw new CompileException(stackTrace,"'name' attribute required");
			this.dotParam = "do";
			this.parameters.add(dotParam);
			this.requiredNamespaces = Collections.emptyList();
			this.naml = new Naml();
			this.naml.add( new EmptyElement(doName,Collections.<Attribute>emptyList(),"",element.lineNumber()) );
			this.extendsNs = asString(attrs.remove("extends"),stackTrace);
		} else if( tagName.equals(Type.TRANSLATION.tagName) ) {
			this.type = Type.TRANSLATION;
			if( !attrs.isEmpty() )
				throw new CompileException(stackTrace,"parameters "+attrs.keySet()+" not allowed in macros/templates");
			if( !(element instanceof Container) )
				throw new CompileException(stackTrace,"empty "+element.name()+" not allowed");
			Container container = (Container)element;
			this.dotParam = null;
			this.requiredNamespaces = Collections.emptyList();
			Iterator<Object> iter = container.contents().iterator();
			Object obj = iter.next();
			if( obj instanceof String )
				obj = iter.next();
			Container from = (Container)obj;
			if( !from.name().equals(fromName) )
				throw new CompileException(stackTrace,"'from' tag expected");
			obj = iter.next();
			if( obj instanceof String )
				obj = iter.next();
			Container to = (Container)obj;
			if( !to.name().equals(toName) )
				throw new CompileException(stackTrace,"'to' tag expected");
			this.name = translationMacroName(from.contents(),parameters);
			this.naml = to.contents();
			this.extendsNs = null;
		} else {
			this.type = tagName.equals(Type.SUBROUTINE.tagName) ? Type.SUBROUTINE : Type.MACRO;
			if( !(element instanceof Container) )
				throw new CompileException(stackTrace,"empty "+element.name()+" not allowed");
			Container container = (Container)element;
			this.name = asString(attrs.remove("name"),stackTrace);
			if( name==null )
				throw new CompileException(stackTrace,"'name' attribute required");
			if( name.indexOf('-') != -1 )
				throw new CompileException(stackTrace,"macro name may not contain '-'");
			parseAttrSet(parameters,asString(attrs.remove("parameters"),stackTrace));
			String dotParam = null;
			{
				List<String> dotParamNames = new ArrayList<String>();
				parseAttrSet(dotParamNames,asString(attrs.remove("dot_parameter"),stackTrace));
				if( dotParamNames.size() > 1 )
					throw new CompileException(stackTrace,"only one dot_parameter allowed");
				if( dotParamNames.size() == 1 ) {
					dotParam = dotParamNames.get(0);
					parameters.add(dotParam);
				}
			}
			this.dotParam = dotParam;
			for( String s : parameters ) {
				if( s.indexOf('-') != -1 )
					throw new CompileException(stackTrace,"macro attibutes may not contain '-'");
			}
			List<String> requiredNamespaces = new ArrayList<String>();
			parseAttrSet(requiredNamespaces,asString(attrs.remove("requires"),stackTrace));
			this.requiredNamespaces = CollectionUtils.optimizeList(requiredNamespaces);
			boolean unindent = "true".equals(asString(attrs.remove("unindent"),stackTrace));
			if( type==Type.SUBROUTINE && this.requiredNamespaces.isEmpty() )
				throw new CompileException(stackTrace,"subroutine must specify required stack");
			if( !attrs.isEmpty() )
				throw new CompileException(stackTrace,"parameters "+attrs.keySet()+" not allowed in macros/templates");
			Naml naml = trim(container.contents());
			if( unindent )
				naml = unindent(naml);
			this.naml = naml;
			this.extendsNs = null;
		}
		{
			StringBuilder buf = new StringBuilder();
			buf.append( name );
			buf.append( '!' ).append( source );
			Integer count = macroCount.get(this.name);
			if( count == null ) {
				macroCount.put(this.name,1);
			} else {
				count += 1;
				macroCount.put(this.name,count);
				buf.append( '!' ).append( count );
			}
			id = buf.toString().intern();
		}
	}

	public static String getNameFromId(String id) {
		return id.substring(0, id.indexOf('!'));
	}

	public static String getSourceFromId(String id) {
		int posColon = id.indexOf(':');
		int posCount = id.indexOf('!', posColon);
		return id.substring(posColon+1, posCount >= 0? posCount : id.length());
	}

	private static final Pattern spacePtn = Pattern.compile("\\s+");

	static String translationMacroName(Naml contents,Set<String> parameters) {
		String text = gutTranslationArgs(contents,parameters).toString();
		return "translation: " + spacePtn.matcher(text).replaceAll(" ");
	}

	private static Naml gutTranslationArgs(Naml oldContents,Set<String> parameters) {
		boolean changed = false;
		Naml newContents = new Naml();
		for( Object obj : oldContents ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				ElementName name = element.name();
				List<ElementName.Part> parts = name.parts();
				if( parts.size() >= 2 && "t".equals(parts.get(0).text()) ) {
					changed = true;
					if( parts.size() > 2 )
						name = new ElementName( false, parts.subList(0,2) );
					Element newElement = new EmptyElement(name,Collections.<Attribute>emptyList(),"",element.lineNumber());
					newContents.add(newElement);
					if( parameters != null )
						parameters.add(parts.get(1).text());
					continue;
				}
				if( element instanceof Container ) {
					Container oldContainer = (Container)element;
					Naml oldContainerContents = oldContainer.contents();
					Naml newContainerContents = gutTranslationArgs(oldContainerContents,parameters);
					if( newContainerContents != oldContainerContents ) {
						changed = true;
						Container newContainer = new Container(oldContainer.name(),oldContainer.attributes(),"",oldContainer.lineNumber(),newContainerContents,"");
						newContents.add(newContainer);
						continue;
					}
				}
			}
			newContents.add(obj);
		}
		return changed ? newContents : oldContents;
	}

	private static String asString(Attribute attr,StackTrace stackTrace) throws CompileException {
		if( attr == null )
			return null;
		Naml value = attr.value();
		if( value.isEmpty() )
			throw new CompileException(stackTrace,"attribute '"+attr.name()+"' must have a value");
		Object obj = value.get(0);
		if( value.size() > 1 || !(obj instanceof String) )
			throw new CompileException(stackTrace,"attribute '"+attr.name()+"' must have string value");
		String s = (String)obj;
		if( s.length()==0 )
			throw new CompileException(stackTrace,"attribute '"+attr.name()+"' may not be empty");
		return s;
	}

	private static void parseAttrSet(Collection<String> col,String attrS) {
		if( attrS != null ) {
			for( String s : attrS.split(",") ) {
				s = s.trim();
				if( !s.equals("") )
					col.add(s);
			}
		}
	}

	static Naml trim(Naml naml) {
		if( naml.size()==0 )
			return naml;
		boolean isChanged = false;
		Object firstObj = naml.get(0);
		if( firstObj instanceof String ) {
			String s = (String)firstObj;
			int i = 0;
			if( Character.isWhitespace(s.charAt(i)) ) {
				isChanged = true;
				naml = new Naml(naml);
				while( ++i < s.length() && Character.isWhitespace(s.charAt(i)) );
				if( i == s.length() ) {
					naml.remove(0);
					if( naml.size()==0 )
						return naml;
				} else {
					naml.set(0,s.substring(i));
				}
			}
		}
		int last = naml.size() - 1;
		Object lastObj = naml.get(last);
		if( lastObj instanceof String ) {
			String s = (String)lastObj;
			int i = s.length() - 1;
			if( Character.isWhitespace(s.charAt(i)) ) {
				if( !isChanged )
					naml = new Naml(naml);
				while( --i >= 0 && Character.isWhitespace(s.charAt(i)) );
				if( i == -1 ) {
					naml.remove(last);
				} else {
					naml.set(last,s.substring(0,i+1));
				}
			}
		}
		return naml;
	}

	private static final Pattern indentPtn = Pattern.compile("([\\r\\n])[ \\t]+");

	private static Naml unindent(Naml naml) {
		boolean isChanged = false;
		Naml newNaml = new Naml(naml);
		for( ListIterator<Object> iter = newNaml.listIterator(); iter.hasNext(); ) {
			Object obj = iter.next();
			if( obj instanceof String ) {
				String s = (String)obj;
				String s2 = indentPtn.matcher(s).replaceAll("$1");
				if( !s.equals(s2) ) {
					isChanged = true;
					iter.set(s2);
				}
			} else if( obj instanceof Cdata ) {
				Cdata cdata = (Cdata)obj;
				String s = cdata.text();
				String s2 = indentPtn.matcher(s).replaceAll("$1");
				if( !s.equals(s2) ) {
					isChanged = true;
					iter.set(new Cdata(s2));
				}
			} else if( obj instanceof Container ) {
				Container c = (Container)obj;
				Naml contents = c.contents();
				Naml contents2 = unindent(contents);
				if( contents != contents2 ) {
					isChanged = true;
					Container c2 = new Container(c.name(),c.attributes(),c.spaceAtEndOfClosingTag(),c.lineNumber(),contents2,c.spaceAtEndOfClosingTag());
					iter.set(c2);
				}
			}
		}
		return isChanged ? newNaml : naml;
	}

	@Override public String getName() {
		return name;
	}

	@Override public Collection<String> getRequiredNamespaces() {
		return requiredNamespaces;
	}

	@Override public String getId() {
		return id;
	}

	@Override public String toString() {
		return name + " - " + source;
	}

	public Type getType() {
		return type;
	}

	public boolean isOverride() {
		return isOverride;
	}

	public String[] getParameterNames() {
		return parameters.toArray(new String[0]);
	}
/*
	@Override public boolean equals(Object obj) {
		if( obj == this )
			return true;
		if( !(obj instanceof Macro) )
			return false;
		Macro m = (Macro)obj;
		return m.key.equals(key) && m.source==source;
	}

	@Override public int hashCode() {
		return key.hashCode() + 31*source.id.hashCode();
	}
*/
}