docs: @Nullable
      public static String replaceMacro(@CheckForNull String s, @Nonnull VariableResolver<String> resolver) {
      	if (s == null) {
      		return null;
      	}
  
          int idx=0;
          while(true) {
              Matcher m = VARIABLE.matcher(s);
              if(!m.find(idx))   return s;
  
              String key = m.group().substring(1);
  
              // escape the dollar sign or get the key to resolve
              String value;
              if(key.charAt(0)=='$') {
                 value = "$";
              } else {
                 if(key.charAt(0)=='{')  key = key.substring(1,key.length()-1);
                 value = resolver.resolve(key);
              }
  
              if(value==null)
                  idx = m.end(); // skip this
              else {
                  s = s.substring(0,m.start())+value+s.substring(m.end());
                  idx = m.start() + value.length();
              }
          }
      }