docs: @VisibleForTesting
    String sanitizeCssProperties(String style) {
      final StringBuilder sanitizedCss = new StringBuilder();
      CssGrammar.parsePropertyGroup(style, new CssGrammar.PropertyHandler() {
        CssSchema.Property cssProperty = CssSchema.DISALLOWED;
        List<CssSchema.Property> cssProperties = null;
        int propertyStart = 0;
        boolean hasTokens;
        boolean inQuotedIdents;
        String lastToken = null;
  
        private void emitToken(String token) {
          closeQuotedIdents();
          if (hasTokens) { sanitizedCss.append(' '); }
          sanitizedCss.append(token);
          hasTokens = true;
        }
  
        private void closeQuotedIdents() {
          if (inQuotedIdents) {
            sanitizedCss.append('\'');
            inQuotedIdents = false;
          }
        }
  
        private void sanitizeAndAppendUrl(String urlContent) {
          if (urlContent.length() < 1024) {
            String rewrittenUrl = urlRewriter.apply(urlContent);
            if (rewrittenUrl != null && !rewrittenUrl.isEmpty()) {
              if (hasTokens) { sanitizedCss.append(' '); }
              sanitizedCss.append("url('").append(rewrittenUrl).append("')");
              hasTokens = true;
            }
          }
        }
  
        public void url(String token) {
          closeQuotedIdents();
          if (cssProperty != null) {
            if ((cssProperty.bits & CssSchema.BIT_URL) != 0) {
              String urlContent = CssGrammar.cssContent(
                  Strings.stripHtmlSpaces(  // TODO: css spaces
                      token.substring(4, token.length() - 1)));
              sanitizeAndAppendUrl(urlContent);
            }
          }
          lastToken = token;
        }
  
        public void startProperty(String propertyName) {
          if (cssProperties != null) { cssProperties.clear(); }
          cssProperty = cssSchema.forKey(propertyName);
          hasTokens = false;
          propertyStart = sanitizedCss.length();
          if (sanitizedCss.length() != 0) {
            sanitizedCss.append(';');
          }
          sanitizedCss.append(propertyName).append(':');
        }
  
        public void startFunction(String uncanonToken) {
          closeQuotedIdents();
          if (cssProperties == null) { cssProperties = Lists.newArrayList(); }
          cssProperties.add(cssProperty);
          String token = Strings.toLowerCase(uncanonToken);
          String key = cssProperty.fnKeys.get(token);
          cssProperty = key != null
              ? cssSchema.forKey(key)
              : CssSchema.DISALLOWED;
          if (cssProperty != CssSchema.DISALLOWED) {
            emitToken(token);
          }
          lastToken = token;
        }
  
        public void quotedString(String token) {
          closeQuotedIdents();
          // The contents of a quoted string could be treated as
          // 1. a run of space-separated words, as in a font family name,
          // 2. as a URL,
          // 3. as plain text content as in a list-item bullet,
          // 4. or it could be ambiguous as when multiple bits are set.
          int meaning =
              cssProperty.bits
              & (CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_URL);
          if ((meaning & (meaning - 1)) == 0) {  // meaning is unambiguous
            if (meaning == CssSchema.BIT_UNRESERVED_WORD
                && token.length() > 2
                && isAlphanumericOrSpaceOrHyphen(token, 1, token.length() - 1)) {
              emitToken(Strings.toLowerCase(token));
            } else if (meaning == CssSchema.BIT_URL) {
              // convert to a URL token and hand-off to the appropriate method
              sanitizeAndAppendUrl(CssGrammar.cssContent(token));
            }
          }
          lastToken = token;
        }
  
        public void quantity(String token) {
          int test = token.startsWith("-")
              ? CssSchema.BIT_NEGATIVE : CssSchema.BIT_QUANTITY;
          if ((cssProperty.bits & test) != 0
              // font-weight uses 100, 200, 300, etc.
              || cssProperty.literals.contains(token)) {
            emitToken(token);
          }
          lastToken = token;
        }
  
        public void punctuation(String token) {
          closeQuotedIdents();
          if (cssProperty.literals.contains(token)) {
            emitToken(token);
          }
          lastToken = token;
        }
  
        private static final int IDENT_TO_STRING =
            CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_STRING;
        public void identifier(String uncanonToken) {
          String token = Strings.toLowerCase(uncanonToken);
          if ("!".equals(lastToken) && "important".equals(token)) {
            emitToken("!important");
          } else if (cssProperty.literals.contains(token)) {
            emitToken(token);
          } else if ((cssProperty.bits & IDENT_TO_STRING) == IDENT_TO_STRING) {
            if (!inQuotedIdents) {
              inQuotedIdents = true;
              if (hasTokens) { sanitizedCss.append(' '); }
              sanitizedCss.append('\'');
              hasTokens = true;
            } else {
              sanitizedCss.append(' ');
            }
            sanitizedCss.append(Strings.toLowerCase(token));
          }
          lastToken = token;
        }
  
        public void hash(String token) {
          closeQuotedIdents();
          if ((cssProperty.bits & CssSchema.BIT_HASH_VALUE) != 0) {
            emitToken(Strings.toLowerCase(token));
          }
          lastToken = token;
        }
  
        public void endProperty() {
          if (!hasTokens) {
            sanitizedCss.setLength(propertyStart);
          } else {
            closeQuotedIdents();
          }
          lastToken = null;
        }
  
        public void endFunction(String token) {
          if (cssProperty != CssSchema.DISALLOWED) { emitToken(")"); }
          cssProperty = cssProperties.remove(cssProperties.size() - 1);
          lastToken = ")";
        }
      });
      return sanitizedCss.length() == 0 ? null : sanitizedCss.toString();
    }