Mercurial Hosting > nabble
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(); + } +*/ +}