From 9405df57c1a09ecd3dec65ea20771fcd1334f942 Mon Sep 17 00:00:00 2001 From: James Roper Date: Tue, 2 Apr 2013 17:16:50 +1100 Subject: [PATCH] Added basic plugin functionality --- src/main/java/org/pegdown/Parser.java | 11 +- .../java/org/pegdown/ToHtmlSerializer.java | 19 ++- .../pegdown/plugins/BlockPluginParser.java | 32 +++++ .../pegdown/plugins/InlinePluginParser.java | 32 +++++ .../org/pegdown/plugins/PegdownPlugins.java | 113 ++++++++++++++++++ .../plugins/ToHtmlSerializerPlugin.java | 39 ++++++ .../java/org/pegdown/BlockPluginNode.java | 16 +++ .../java/org/pegdown/InlinePluginNode.java | 16 +++ src/test/java/org/pegdown/PluginParser.java | 49 ++++++++ src/test/resources/pegdown/Plugins.html | 5 + src/test/resources/pegdown/Plugins.md | 5 + .../org/pegdown/AbstractPegDownSpec.scala | 48 +++++--- src/test/scala/org/pegdown/PegDownSpec.scala | 29 +++++ 13 files changed, 390 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/pegdown/plugins/BlockPluginParser.java create mode 100644 src/main/java/org/pegdown/plugins/InlinePluginParser.java create mode 100644 src/main/java/org/pegdown/plugins/PegdownPlugins.java create mode 100644 src/main/java/org/pegdown/plugins/ToHtmlSerializerPlugin.java create mode 100644 src/test/java/org/pegdown/BlockPluginNode.java create mode 100644 src/test/java/org/pegdown/InlinePluginNode.java create mode 100644 src/test/java/org/pegdown/PluginParser.java create mode 100644 src/test/resources/pegdown/Plugins.html create mode 100644 src/test/resources/pegdown/Plugins.md diff --git a/src/main/java/org/pegdown/Parser.java b/src/main/java/org/pegdown/Parser.java index ade4f97..660eabe 100644 --- a/src/main/java/org/pegdown/Parser.java +++ b/src/main/java/org/pegdown/Parser.java @@ -32,6 +32,7 @@ import org.parboiled.support.Var; import org.pegdown.ast.*; import org.pegdown.ast.SimpleNode.Type; +import org.pegdown.plugins.PegdownPlugins; import java.util.ArrayList; import java.util.Arrays; @@ -63,14 +64,20 @@ public ParseRunner get(Rule rule) { protected final int options; protected final long maxParsingTimeInMillis; protected final ParseRunnerProvider parseRunnerProvider; + protected final PegdownPlugins plugins; final List abbreviations = new ArrayList(); final List references = new ArrayList(); long parsingStartTimeStamp = 0L; - public Parser(Integer options, Long maxParsingTimeInMillis, ParseRunnerProvider parseRunnerProvider) { + public Parser(Integer options, Long maxParsingTimeInMillis, ParseRunnerProvider parseRunnerProvider, PegdownPlugins plugins) { this.options = options; this.maxParsingTimeInMillis = maxParsingTimeInMillis; this.parseRunnerProvider = parseRunnerProvider; + this.plugins = plugins; + } + + public Parser(Integer options, Long maxParsingTimeInMillis, ParseRunnerProvider parseRunnerProvider) { + this(options, maxParsingTimeInMillis, parseRunnerProvider, PegdownPlugins.NONE); } public RootNode parse(char[] source) { @@ -98,6 +105,7 @@ public Rule Block() { return Sequence( ZeroOrMore(BlankLine()), FirstOf(new ArrayBuilder() + .add(plugins.getBlockPluginRules()) .add(BlockQuote(), Verbatim()) .addNonNulls(ext(ABBREVIATIONS) ? Abbreviation() : null) .add(Reference(), HorizontalRule(), Heading(), OrderedList(), BulletList(), HtmlBlock()) @@ -569,6 +577,7 @@ public Rule NonAutoLinkInline() { public Rule NonLinkInline() { return FirstOf(new ArrayBuilder() + .add(plugins.getInlinePluginRules()) .add(Str(), Endline(), UlOrStarLine(), Space(), StrongOrEmph(), Image(), Code(), InlineHtml(), Entity(), EscapedChar()) .addNonNulls(ext(QUOTES) ? new Rule[]{SingleQuoted(), DoubleQuoted(), DoubleAngleQuoted()} : null) diff --git a/src/main/java/org/pegdown/ToHtmlSerializer.java b/src/main/java/org/pegdown/ToHtmlSerializer.java index 7a31c89..1aec036 100644 --- a/src/main/java/org/pegdown/ToHtmlSerializer.java +++ b/src/main/java/org/pegdown/ToHtmlSerializer.java @@ -20,11 +20,9 @@ import org.parboiled.common.StringUtils; import org.pegdown.ast.*; +import org.pegdown.plugins.ToHtmlSerializerPlugin; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; import static org.parboiled.common.Preconditions.checkArgNotNull; @@ -34,13 +32,19 @@ public class ToHtmlSerializer implements Visitor { protected final Map references = new HashMap(); protected final Map abbreviations = new HashMap(); protected final LinkRenderer linkRenderer; + protected final List plugins; protected TableNode currentTableNode; protected int currentTableColumn; protected boolean inTableHeader; public ToHtmlSerializer(LinkRenderer linkRenderer) { + this(linkRenderer, Collections.emptyList()); + } + + public ToHtmlSerializer(LinkRenderer linkRenderer, List plugins) { this.linkRenderer = linkRenderer; + this.plugins = plugins; } public String toHtml(RootNode astRoot) { @@ -323,8 +327,13 @@ public void visit(SuperNode node) { } public void visit(Node node) { + for (ToHtmlSerializerPlugin plugin : plugins) { + if (plugin.visit(node, this, printer)) { + return; + } + } // override this method for processing custom Node implementations - throw new RuntimeException("Not implemented"); + throw new RuntimeException("Don't know how to handle node " + node); } // helpers diff --git a/src/main/java/org/pegdown/plugins/BlockPluginParser.java b/src/main/java/org/pegdown/plugins/BlockPluginParser.java new file mode 100644 index 0000000..643e13a --- /dev/null +++ b/src/main/java/org/pegdown/plugins/BlockPluginParser.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010-2011 Mathias Doenitz + * + * Based on peg-markdown (C) 2008-2010 John MacFarlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.pegdown.plugins; + +import org.parboiled.Rule; + +/** + * A parser that provides block parser rules for pegdown. A pegdown plugin should implement this, along with + * {@link org.parboiled.BaseParser} or {@link org.pegdown.Parser} if it wants to use the pegdown utilities. + * + * This interface is intended for use with {@link PegdownPlugins.Builder#withPlugin(Class, Object...)}, in order for + * Java plugins to easily be registered with a parser. + */ +public interface BlockPluginParser { + Rule[] blockPluginRules(); +} diff --git a/src/main/java/org/pegdown/plugins/InlinePluginParser.java b/src/main/java/org/pegdown/plugins/InlinePluginParser.java new file mode 100644 index 0000000..a67fd9d --- /dev/null +++ b/src/main/java/org/pegdown/plugins/InlinePluginParser.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010-2011 Mathias Doenitz + * + * Based on peg-markdown (C) 2008-2010 John MacFarlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.pegdown.plugins; + +import org.parboiled.Rule; + +/** + * A parser that provides inline parser rules for pegdown. A pegdown plugin should implement this, along with + * {@link org.parboiled.BaseParser} or {@link org.pegdown.Parser} if it wants to use the pegdown utilities. + * + * This interface is intended for use with {@link PegdownPlugins.Builder#withPlugin(Class, Object...)}, in order for + * Java plugins to easily be registered with a parser. + */ +public interface InlinePluginParser { + Rule[] inlinePluginRules(); +} diff --git a/src/main/java/org/pegdown/plugins/PegdownPlugins.java b/src/main/java/org/pegdown/plugins/PegdownPlugins.java new file mode 100644 index 0000000..9959855 --- /dev/null +++ b/src/main/java/org/pegdown/plugins/PegdownPlugins.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2010-2011 Mathias Doenitz + * + * Based on peg-markdown (C) 2008-2010 John MacFarlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.pegdown.plugins; + +import org.parboiled.BaseParser; +import org.parboiled.Parboiled; +import org.parboiled.Rule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Encapsulates the plugins provided to pegdown. + * + * Construct this using @{link PegdownPlugins#builder}, and then passing in either the Java plugin classes, or + * precompiled rules (for greater control, or if using Scala rules). + */ +public class PegdownPlugins { + + private final Rule[] inlinePluginRules; + private final Rule[] blockPluginRules; + + private PegdownPlugins(Rule[] inlinePluginRules, Rule[] blockPluginRules) { + this.inlinePluginRules = inlinePluginRules; + this.blockPluginRules = blockPluginRules; + } + + public Rule[] getInlinePluginRules() { + return inlinePluginRules; + } + + public Rule[] getBlockPluginRules() { + return blockPluginRules; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Create a builder that is a copy of the existing plugins + */ + public static Builder builder(PegdownPlugins like) { + return builder().withInlinePluginRules(like.getInlinePluginRules()).withBlockPluginRules(like.getBlockPluginRules()); + } + + /** + * Convenience reference to no plugins. + */ + public static PegdownPlugins NONE = builder().build(); + + public static class Builder { + private final List inlinePluginRules = new ArrayList(); + private final List blockPluginRules = new ArrayList(); + + public Builder() { + } + + public Builder withInlinePluginRules(Rule... inlinePlugins) { + this.inlinePluginRules.addAll(Arrays.asList(inlinePlugins)); + return this; + } + + public Builder withBlockPluginRules(Rule... blockPlugins) { + this.blockPluginRules.addAll(Arrays.asList(blockPlugins)); + return this; + } + + /** + * Add a plugin parser. This should either implement {@link InlinePluginParser} or {@link BlockPluginParser}, + * or both. The parser will be enhanced by parboiled before its rules are extracted and registered here. + * + * @param pluginParser the plugin parser class. + * @param arguments the arguments to pass to the constructor of that class. + */ + public Builder withPlugin(Class> pluginParser, Object... arguments) { + // First, check that the parser implements one of the parser interfaces + if (!(InlinePluginParser.class.isAssignableFrom(pluginParser) || + BlockPluginParser.class.isAssignableFrom(pluginParser))) { + throw new IllegalArgumentException("Parser plugin must implement a parser plugin interface to be useful"); + } + BaseParser parser = Parboiled.createParser(pluginParser, arguments); + if (parser instanceof InlinePluginParser) { + withInlinePluginRules(((InlinePluginParser) parser).inlinePluginRules()); + } + if (parser instanceof BlockPluginParser) { + withBlockPluginRules(((BlockPluginParser) parser).blockPluginRules()); + } + return this; + } + + public PegdownPlugins build() { + return new PegdownPlugins(inlinePluginRules.toArray(new Rule[0]), blockPluginRules.toArray(new Rule[0])); + } + } +} diff --git a/src/main/java/org/pegdown/plugins/ToHtmlSerializerPlugin.java b/src/main/java/org/pegdown/plugins/ToHtmlSerializerPlugin.java new file mode 100644 index 0000000..58de94c --- /dev/null +++ b/src/main/java/org/pegdown/plugins/ToHtmlSerializerPlugin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010-2011 Mathias Doenitz + * + * Based on peg-markdown (C) 2008-2010 John MacFarlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.pegdown.plugins; + +import org.pegdown.Printer; +import org.pegdown.ast.Node; +import org.pegdown.ast.Visitor; + +/** + * A plugin for the {@link org.pegdown.ToHtmlSerializer} + */ +public interface ToHtmlSerializerPlugin { + + /** + * Visit the given node + * + * @param node The node to visit + * @param visitor The visitor, for delegating back to handling children, etc + * @param printer The printer to print output to + * @return true if this plugin knew how to serialize the node, false otherwise + */ + boolean visit(Node node, Visitor visitor, Printer printer); +} diff --git a/src/test/java/org/pegdown/BlockPluginNode.java b/src/test/java/org/pegdown/BlockPluginNode.java new file mode 100644 index 0000000..51fae1d --- /dev/null +++ b/src/test/java/org/pegdown/BlockPluginNode.java @@ -0,0 +1,16 @@ +package org.pegdown; + +import org.pegdown.ast.Node; +import org.pegdown.ast.TextNode; +import org.pegdown.ast.Visitor; + +public class BlockPluginNode extends TextNode { + public BlockPluginNode(String text) { + super(text); + } + + @Override + public void accept(Visitor visitor) { + visitor.visit((Node) this); + } +} diff --git a/src/test/java/org/pegdown/InlinePluginNode.java b/src/test/java/org/pegdown/InlinePluginNode.java new file mode 100644 index 0000000..f22c884 --- /dev/null +++ b/src/test/java/org/pegdown/InlinePluginNode.java @@ -0,0 +1,16 @@ +package org.pegdown; + +import org.pegdown.ast.Node; +import org.pegdown.ast.TextNode; +import org.pegdown.ast.Visitor; + +public class InlinePluginNode extends TextNode { + public InlinePluginNode(String text) { + super(text); + } + + @Override + public void accept(Visitor visitor) { + visitor.visit((Node) this); + } +} diff --git a/src/test/java/org/pegdown/PluginParser.java b/src/test/java/org/pegdown/PluginParser.java new file mode 100644 index 0000000..4d3e584 --- /dev/null +++ b/src/test/java/org/pegdown/PluginParser.java @@ -0,0 +1,49 @@ +package org.pegdown; + +import org.parboiled.BaseParser; +import org.parboiled.Rule; +import org.parboiled.support.StringBuilderVar; +import org.pegdown.plugins.BlockPluginParser; +import org.pegdown.plugins.InlinePluginParser; + +public class PluginParser extends Parser implements InlinePluginParser, BlockPluginParser { + + public PluginParser() { + super(ALL, 1000l, DefaultParseRunnerProvider); + } + + @Override + public Rule[] blockPluginRules() { + return new Rule[] {BlockPlugin()}; + } + + @Override + public Rule[] inlinePluginRules() { + return new Rule[] {InlinePlugin()}; + } + + public Rule InlinePlugin() { + StringBuilderVar text = new StringBuilderVar(); + return NodeSequence( + Ch('%'), + OneOrMore(TestNot(Ch('%')), BaseParser.ANY, text.append(matchedChar())), + push(new InlinePluginNode(text.getString())), + Ch('%') + ); + } + + public Rule BlockPlugin() { + StringBuilderVar text = new StringBuilderVar(); + return NodeSequence( + BlockPluginMarker(), + OneOrMore(TestNot(Newline(), BlockPluginMarker()), BaseParser.ANY, text.append(matchedChar())), + Newline(), + push(new BlockPluginNode(text.appended('\n').getString())), + BlockPluginMarker() + ); + } + + public Rule BlockPluginMarker() { + return Sequence(NOrMore('%', 3), Newline()); + } +} diff --git a/src/test/resources/pegdown/Plugins.html b/src/test/resources/pegdown/Plugins.html new file mode 100644 index 0000000..a4c4747 --- /dev/null +++ b/src/test/resources/pegdown/Plugins.html @@ -0,0 +1,5 @@ +
+A block plugin +
+ +

An inline plugin

\ No newline at end of file diff --git a/src/test/resources/pegdown/Plugins.md b/src/test/resources/pegdown/Plugins.md new file mode 100644 index 0000000..15db206 --- /dev/null +++ b/src/test/resources/pegdown/Plugins.md @@ -0,0 +1,5 @@ +%%% +A block plugin +%%% + +%An inline plugin% \ No newline at end of file diff --git a/src/test/scala/org/pegdown/AbstractPegDownSpec.scala b/src/test/scala/org/pegdown/AbstractPegDownSpec.scala index c9db5d7..0b5e835 100644 --- a/src/test/scala/org/pegdown/AbstractPegDownSpec.scala +++ b/src/test/scala/org/pegdown/AbstractPegDownSpec.scala @@ -12,17 +12,44 @@ import ast.Node abstract class AbstractPegDownSpec extends Specification { def test(testName: String)(implicit processor: PegDownProcessor) { + implicit val serializer = new ToHtmlSerializer(new LinkRenderer) + testWithSerializer(testName) + } + + def test(testName: String, expectedOutput: String)(implicit processor: PegDownProcessor) { + implicit val serializer = new ToHtmlSerializer(new LinkRenderer) + testWithSerializer(testName, expectedOutput) + } + + def testAST(testName: String)(implicit processor: PegDownProcessor) { + val markdown = FileUtils.readAllCharsFromResource(testName + ".md") + require(markdown != null, "Test '" + testName + "' not found") + + val expectedAst = FileUtils.readAllTextFromResource(testName + ".ast") + require(expectedAst != null, "Expected AST for '" + testName + "' not found") + + val astRoot = processor.parseMarkdown(markdown) + + // check parse tree + //assertEquals(printNodeTree(getProcessor().parser.parseToParsingResult(markdown)), ""); + + normalize(GraphUtils.printTree(astRoot, new ToStringFormatter[Node]())) === normalize(expectedAst) + } + + def testWithSerializer(testName: String)(implicit processor: PegDownProcessor, + serializer: ToHtmlSerializer) { val expectedUntidy = FileUtils.readAllTextFromResource(testName + ".html") require(expectedUntidy != null, "Test '" + testName + "' not found") - test(testName, tidy(expectedUntidy)) + testWithSerializer(testName, tidy(expectedUntidy)) } - def test(testName: String, expectedOutput: String)(implicit processor: PegDownProcessor) { + def testWithSerializer(testName: String, expectedOutput: String)(implicit processor: PegDownProcessor, + serializer: ToHtmlSerializer) { val markdown = FileUtils.readAllCharsFromResource(testName + ".md") require(markdown != null, "Test '" + testName + "' not found") val astRoot = processor.parseMarkdown(markdown) - val actualHtml = new ToHtmlSerializer(new LinkRenderer).toHtml(astRoot) + val actualHtml = serializer.toHtml(astRoot) // debugging I: check the parse tree //assertEquals(printNodeTree(getProcessor().parser.parseToParsingResult(markdown)), ""); @@ -38,21 +65,6 @@ abstract class AbstractPegDownSpec extends Specification { normalize(tidyHtml) === normalize(expectedOutput) } - def testAST(testName: String)(implicit processor: PegDownProcessor) { - val markdown = FileUtils.readAllCharsFromResource(testName + ".md") - require(markdown != null, "Test '" + testName + "' not found") - - val expectedAst = FileUtils.readAllTextFromResource(testName + ".ast") - require(expectedAst != null, "Expected AST for '" + testName + "' not found") - - val astRoot = processor.parseMarkdown(markdown) - - // check parse tree - //assertEquals(printNodeTree(getProcessor().parser.parseToParsingResult(markdown)), ""); - - normalize(GraphUtils.printTree(astRoot, new ToStringFormatter[Node]())) === normalize(expectedAst) - } - def tidy(html: String) = { val in = new StringReader(html) val out = new StringWriter diff --git a/src/test/scala/org/pegdown/PegDownSpec.scala b/src/test/scala/org/pegdown/PegDownSpec.scala index 1d2c8eb..6e4a48e 100644 --- a/src/test/scala/org/pegdown/PegDownSpec.scala +++ b/src/test/scala/org/pegdown/PegDownSpec.scala @@ -1,7 +1,9 @@ package org.pegdown +import ast.{Visitor, Node} import org.parboiled.Parboiled import Extensions._ +import plugins.{ToHtmlSerializerPlugin, PegdownPlugins} class PegDownSpec extends AbstractPegDownSpec { @@ -88,6 +90,33 @@ class PegDownSpec extends AbstractPegDownSpec { )(new PegDownProcessor(SUPPRESS_ALL_HTML)) } } + + "allow custom plugins" in { + import scala.collection.JavaConversions._ + implicit val processor = new PegDownProcessor(Parboiled.createParser[Parser, AnyRef](classOf[Parser], + new java.lang.Integer(ALL), new java.lang.Long(1000), Parser.DefaultParseRunnerProvider, + PegdownPlugins.builder().withPlugin(classOf[PluginParser]).build())) + implicit val serializer = new ToHtmlSerializer(new LinkRenderer, List(new ToHtmlSerializerPlugin { + def visit(node: Node, visitor: Visitor, printer: Printer) = node match { + case blockPlugin: BlockPluginNode => { + printer.print("
") + printer.print(blockPlugin.getText) + printer.print("
") + true + } + case inlinePlugin: InlinePluginNode => { + printer.print("") + printer.print(inlinePlugin.getText) + printer.print("") + true + } + case _ => false + } + })) + + testWithSerializer("pegdown/Plugins") + } + } }