diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/nabble/naml/compiler/Macro.java	Thu Mar 21 19:15:52 2019 -0600
@@ -0,0 +1,357 @@
+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();
+	}
+*/
+}