/*
* RMarkdownChunkHeaderParser.java
*
* Copyright (C) 2009-16 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.common.r.knitr;
import java.util.HashMap;
import java.util.Map;
import org.rstudio.core.client.Mutable;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.TextCursor;
import org.rstudio.core.client.regex.Match;
import org.rstudio.core.client.regex.Pattern;
public class RMarkdownChunkHeaderParser
{
public static Map<String, String> parse(String line)
{
Map<String, String> options = new HashMap<String, String>();
parse(line, options);
return options;
}
public static final void parse(String line, Map<String, String> options)
{
// set up state
Mutable<String> key = new Mutable<String>();
Consumer keyConsumer = new MutableConsumer(key);
Mutable<String> val = new Mutable<String>();
Consumer valConsumer = new MutableConsumer(val);
line = trimBoundary(line);
TextCursor cursor = new TextCursor(line);
// force default R engine
options.put("engine", ensureQuoted("r"));
// consume engine
if (!consumeEngine(cursor, options))
return;
// consume whitespace and commas
if (!cursor.consumeUntilRegex("[^\\s,]"))
return;
// consume next token -- need to determine
// whether this is a chunk option name or
// a label soon after
if (!consumeKey(cursor, keyConsumer))
return;
// consume until ',' or '='. if nothing is
// found, this must have been a label
if (!cursor.consumeUntilRegex("[,=]"))
{
options.put("label", ensureQuoted(key.get().trim()));
return;
}
char ch = cursor.peek();
if (ch == ',')
{
// found a comma -- this must have been a label
options.put("label", ensureQuoted(key.get().trim()));
}
else
{
// found an '=' -- this was a key for a chunk option
if (!cursor.consume('='))
return;
// eat whitespace
if (!cursor.consumeUntilRegex("\\S"))
return;
// consume value
if (!consumeValue(cursor, valConsumer))
return;
// set option
options.put(key.get(), val.get());
// move to next comma
if (!cursor.fwdToCharacter(',', false))
return;
}
while (cursor.peek() == ',')
{
// eat whitespace and commas
if (!cursor.consumeUntilRegex("[^\\s,]"))
return;
// consume key
if (!consumeKey(cursor, keyConsumer))
return;
// eat whitespace
if (!cursor.consumeUntilRegex("\\S"))
return;
// check '='
if (!cursor.consume('='))
return;
// eat whitespace
if (!cursor.consumeUntilRegex("\\S"))
return;
// consume value
if (!consumeValue(cursor, valConsumer))
return;
// update options
options.put(
StringUtil.stringValue(key.get().trim()),
val.get().trim());
// find next comma
if (!cursor.consumeUntil(','))
return;
}
return;
}
private static final String trimBoundary(String line)
{
return line
.replaceAll("^\\s*```{\\s*", "")
.replaceAll("\\s*}\\s*$", "")
.trim();
}
private static final boolean consumeEngine(final TextCursor cursor,
final Map<String, String> options)
{
Consumer consumer = new Consumer()
{
@Override
public void consume(String value)
{
options.put("engine", ensureQuoted(value));
}
};
if (consumeUntilRegex(cursor, "(?:$|[\\s,])", consumer))
return true;
return false;
}
private static final boolean consumeUntilRegex(TextCursor cursor,
String regex,
Consumer consumer)
{
Pattern pattern = Pattern.create(regex);
Match match = pattern.match(cursor.getData(), cursor.getIndex());
if (match == null)
return false;
int startIdx = cursor.getIndex();
int endIdx = match.getIndex();
if (consumer != null)
{
String value = cursor.getData().substring(startIdx, endIdx);
consumer.consume(value);
}
cursor.setIndex(endIdx);
return true;
}
private static final boolean consumeQuotedItem(TextCursor cursor, Consumer consumer)
{
if (!isQuote(cursor.peek()))
return false;
int startIndex = cursor.getIndex();
if (cursor.fwdToMatchingCharacter())
{
int endIndex = cursor.getIndex() + 1;
if (consumer != null)
{
String value = cursor.getData().substring(startIndex, endIndex);
consumer.consume(value);
}
cursor.setIndex(endIndex);
return true;
}
return false;
}
private static final boolean consumeKey(TextCursor cursor, Consumer consumer)
{
if (isQuote(cursor.peek()) && consumeQuotedItem(cursor, consumer))
return true;
return consumeUntilRegex(cursor, "(?:$|[^a-zA-Z0-9_.])", consumer);
}
private static final boolean consumeValue(TextCursor cursor,
Consumer consumer)
{
if (consumeQuotedItem(cursor, consumer))
return true;
int startIdx = cursor.getIndex();
do
{
if (cursor.isLeftBracket() && cursor.fwdToMatchingCharacter())
continue;
if (cursor.peek() == ',')
break;
}
while (cursor.moveToNextCharacter());
int endIdx = cursor.getIndex();
if (consumer != null)
{
String value = cursor.getData().substring(startIdx, endIdx);
consumer.consume(value);
}
cursor.setIndex(endIdx);
return true;
}
private static final boolean isQuote(char ch)
{
return ch == '\'' || ch == '"' || ch == '`';
}
private static final String ensureQuoted(String string)
{
String[] quotes = new String[] { "\"", "'", "`" };
for (String quote : quotes)
if (string.startsWith(quote) && string.endsWith(quote))
return string;
return "\"" + string.replaceAll("\"", "\\\\\"") + "\"";
}
private static interface Consumer
{
public void consume(String value);
}
private static class MutableConsumer implements Consumer
{
public MutableConsumer(Mutable<String> value)
{
value_ = value;
}
@Override
public void consume(String value)
{
value_.set(value);
}
private final Mutable<String> value_;
}
}