Repository: cobalt-org/liquid-rust
Pull request #357 (2019-09-06 21:20:12)

Iterate on reflection support
This is everything but the changes to `liquid-bin` to support and provide a demo for printing out what is supported by the parser.

This is a part of #204 


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 53330dd..fe08379 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,13 @@
 [workspace]
-members = ["liquid-error", "liquid-value", "liquid-compiler", "liquid-interpreter", "liquid-derive"]
+members = [
+  "liquid-error",
+  "liquid-value",
+  "liquid-compiler",
+  "liquid-interpreter",
+  "liquid-derive",
+  "liquid-help-md",
+  "liquid-bin",
+]
 
 [package]
 name = "liquid"
@@ -16,17 +24,8 @@ license = "MIT"
 [badges]
 azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
 
-[[bin]]
-name = "liquid-dbg"
-required-features = ["cli", "serde"]
-test = false
-doctest = false
-bench = false
-doc = false
-
 [features]
 default = ["extra-filters", "serde"]
-cli = ["clap", "error-chain", "serde_yaml", "serde_json"]
 extra-filters = []
 # Ensure keys in `Value`s `Object`s to be sorted.
 object_sorted = []
@@ -47,10 +46,6 @@ liquid-interpreter = { version = "^0.19", path = "liquid-interpreter" }
 liquid-derive = { version = "^0.19", path = "liquid-derive" }
 
 serde = { version = "1.0", optional = true, features = ["derive"] }
-clap = { version = "2.26", optional = true }
-error-chain = { version = "0.12.0", optional = true }
-serde_yaml = { version = "0.8", optional = true }
-serde_json = { version = "1.0", optional = true }
 deunicode = "1.0.0"
 
 [dev-dependencies]
diff --git a/liquid-bin/Cargo.toml b/liquid-bin/Cargo.toml
new file mode 100644
index 0000000..0263933
--- /dev/null
+++ b/liquid-bin/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "liquid-bin"
+version = "0.19.0"
+authors = ["Ed Page <eopage@gmail.com>"]
+description = "The liquid templating language for Rust"
+repository = "https://github.com/cobalt-org/liquid-rust"
+documentation = "https://docs.rs/liquid"
+readme = "README.md"
+categories = ["template-engine"]
+keywords = ["liquid", "template", "templating", "language", "html"]
+license = "MIT"
+
+[badges]
+azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
+
+[dependencies]
+liquid = { version = "0.19", path = "../" }
+serde = { version = "1.0", features = ["derive"] }
+structopt = "0.3"
+serde_yaml = "0.8"
+serde_json = "1.0"
+derive_more = "0.15.0"
diff --git a/liquid-bin/LICENSE b/liquid-bin/LICENSE
new file mode 100644
index 0000000..135cef1
--- /dev/null
+++ b/liquid-bin/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 cobalt-org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/liquid-bin/README.md b/liquid-bin/README.md
new file mode 100644
index 0000000..7027002
--- /dev/null
+++ b/liquid-bin/README.md
@@ -0,0 +1,24 @@
+liquid-compiler
+===========
+
+> [Liquid templating](http://liquidmarkup.org/) for Rust
+
+[![Build Status](https://dev.azure.com/cobalt-org/cobalt-org/_apis/build/status/liquid-rust?branchName=master)](https://dev.azure.com/cobalt-org/cobalt-org/_build/latest?definitionId=1&branchName=master)
+[![Crates Status](https://img.shields.io/crates/v/liquid.svg)](https://crates.io/crates/liquid)
+[![Dependency Status](https://dependencyci.com/github/cobalt-org/liquid-rust/badge)](https://dependencyci.com/github/cobalt-org/liquid-rust)
+
+Usage
+----------
+
+To include liquid in your project add the following to your Cargo.toml:
+
+```toml
+[dependencies]
+liquid-compiler = "0.16"
+```
+
+Now you can use the crate in your code:
+
+```rust
+extern crate liquid_compiler;
+```
diff --git a/liquid-bin/src/main.rs b/liquid-bin/src/main.rs
new file mode 100644
index 0000000..36a2808
--- /dev/null
+++ b/liquid-bin/src/main.rs
@@ -0,0 +1,97 @@
+// Allow zero pointers for lazy_static. Otherwise clippy will complain.
+#![allow(unknown_lints)]
+
+extern crate liquid;
+extern crate structopt;
+
+extern crate serde_json;
+extern crate serde_yaml;
+
+use std::ffi;
+use std::fs;
+use std::io::Write;
+use std::path;
+
+use structopt::StructOpt;
+
+#[derive(Copy, Clone, Debug, derive_more::Display, derive_more::From, derive_more::Constructor)]
+#[display(fmt = "{}", msg)]
+struct Error {
+    msg: &'static str,
+}
+
+impl std::error::Error for Error {}
+
+fn load_yaml(path: &path::Path) -> Result<liquid::value::Value, Box<dyn std::error::Error>> {
+    let f = fs::File::open(path)?;
+    serde_yaml::from_reader(f).map_err(|e| e.into())
+}
+
+fn load_json(path: &path::Path) -> Result<liquid::value::Value, Box<dyn std::error::Error>> {
+    let f = fs::File::open(path)?;
+    serde_json::from_reader(f).map_err(|e| e.into())
+}
+
+fn build_context(path: &path::Path) -> Result<liquid::value::Object, Box<dyn std::error::Error>> {
+    let extension = path.extension().unwrap_or_else(|| ffi::OsStr::new(""));
+    let value = if extension == ffi::OsStr::new("yaml") {
+        load_yaml(path)
+    } else if extension == ffi::OsStr::new("yaml") {
+        load_json(path)
+    } else {
+        Err(Error::new("Unsupported file type"))?
+    }?;
+    let value = match value {
+        liquid::value::Value::Object(o) => Ok(o),
+        _ => Err(Error::new("File must be an object")),
+    }?;
+
+    Ok(value)
+}
+
+#[derive(StructOpt)]
+struct Args {
+    #[structopt(long, parse(from_os_str))]
+    input: std::path::PathBuf,
+
+    #[structopt(long, parse(from_os_str))]
+    output: Option<std::path::PathBuf>,
+
+    #[structopt(long, parse(from_os_str))]
+    context: Option<std::path::PathBuf>,
+}
+
+fn run() -> Result<i32, Box<dyn std::error::Error>> {
+    let args = Args::from_args();
+
+    let parser = liquid::ParserBuilder::with_liquid()
+        .extra_filters()
+        .jekyll_filters()
+        .build()
+        .expect("should succeed without partials");
+    let template = parser.parse_file(&args.input)?;
+
+    let data = args
+        .context
+        .as_ref()
+        .map(|p| build_context(p.as_path()))
+        .map_or(Ok(None), |r| r.map(Some))?
+        .unwrap_or_else(liquid::value::Object::new);
+    let output = template.render(&data)?;
+    match args.output {
+        Some(path) => {
+            let mut out = fs::File::create(path)?;
+            out.write_all(output.as_bytes())?;
+        }
+        None => {
+            println!("{}", output);
+        }
+    }
+
+    Ok(0)
+}
+
+fn main() {
+    let code = run().unwrap();
+    std::process::exit(code);
+}
diff --git a/liquid-compiler/src/block.rs b/liquid-compiler/src/block.rs
index 94d4a49..3750721 100644
--- a/liquid-compiler/src/block.rs
+++ b/liquid-compiler/src/block.rs
@@ -29,13 +29,15 @@ pub trait BlockReflection {
 /// of the block, the argument [Tokens](lexer/enum.Token.html) passed to
 /// the block, a Vec of all [Elements](lexer/enum.Element.html) inside the block and
 /// the global [`Language`](struct.Language.html).
-pub trait ParseBlock: Send + Sync + ParseBlockClone + BlockReflection {
+pub trait ParseBlock: Send + Sync + ParseBlockClone {
     fn parse(
         &self,
         arguments: TagTokenIter,
         block: TagBlock,
         options: &Language,
     ) -> Result<Box<dyn Renderable>>;
+
+    fn reflection(&self) -> &dyn BlockReflection;
 }
 
 pub trait ParseBlockClone {
diff --git a/liquid-compiler/src/filter.rs b/liquid-compiler/src/filter.rs
index 9c5c56c..c882670 100644
--- a/liquid-compiler/src/filter.rs
+++ b/liquid-compiler/src/filter.rs
@@ -211,16 +211,18 @@ pub trait Filter: Send + Sync + Debug + Display {
 ///         let args = ExampleArgs::from_args(arguments)?;
 ///         // Use the configuration state of the `ParseFilter`
 ///         let state = self.state;
-///         
+///
 ///         // Create the `Filter` struct and return it, passing the information
 ///         // about the arguments and the configuration of the `ParseFilter`.
 ///         Ok(Box::new(ExampleFilter { args, state }))
 ///     }
 /// }
 /// ```
-pub trait ParseFilter: Send + Sync + ParseFilterClone + FilterReflection {
+pub trait ParseFilter: Send + Sync + ParseFilterClone {
     /// Filter `input` based on `arguments`.
     fn parse(&self, arguments: FilterArguments) -> Result<Box<dyn Filter>>;
+
+    fn reflection(&self) -> &dyn FilterReflection;
 }
 
 /// Support cloning of `Box<ParseFilter>`.
diff --git a/liquid-compiler/src/registry.rs b/liquid-compiler/src/registry.rs
index 1781f57..1904c5d 100644
--- a/liquid-compiler/src/registry.rs
+++ b/liquid-compiler/src/registry.rs
@@ -39,6 +39,11 @@ impl<P> PluginRegistry<P> {
             iter: self.plugins.keys(),
         }
     }
+
+    /// All plugins
+    pub fn plugins(&self) -> impl Iterator<Item = &P> {
+        self.plugins.values()
+    }
 }
 
 impl<P> Default for PluginRegistry<P> {
diff --git a/liquid-compiler/src/tag.rs b/liquid-compiler/src/tag.rs
index 62f6363..e31bbb4 100644
--- a/liquid-compiler/src/tag.rs
+++ b/liquid-compiler/src/tag.rs
@@ -24,8 +24,10 @@ pub trait TagReflection {
 /// a new [Renderable](trait.Renderable.html) based on its parameters. The received parameters
 /// specify the name of the tag, the argument [Tokens](lexer/enum.Token.html) passed to
 /// the tag and the global [`Language`](struct.Language.html).
-pub trait ParseTag: Send + Sync + ParseTagClone + TagReflection {
+pub trait ParseTag: Send + Sync + ParseTagClone {
     fn parse(&self, arguments: TagTokenIter, options: &Language) -> Result<Box<dyn Renderable>>;
+
+    fn reflection(&self) -> &dyn TagReflection;
 }
 
 pub trait ParseTagClone {
diff --git a/liquid-derive/src/parse_filter/parse_filter.rs b/liquid-derive/src/parse_filter/parse_filter.rs
index 30fef70..46be383 100644
--- a/liquid-derive/src/parse_filter/parse_filter.rs
+++ b/liquid-derive/src/parse_filter/parse_filter.rs
@@ -27,6 +27,10 @@ fn generate_parse_filter(filter_parser: &ParseFilter) -> Result<TokenStream> {
                     #build_filter_parameters
                     #return_expr
                 }
+
+                fn reflection(&self) -> &::liquid::compiler::FilterReflection {
+                    self
+                }
             }
         })
     } else {
@@ -47,6 +51,10 @@ fn generate_parse_filter(filter_parser: &ParseFilter) -> Result<TokenStream> {
 
                     #return_expr
                 }
+
+                fn reflection(&self) -> &::liquid::compiler::FilterReflection {
+                    self
+                }
             }
         })
     }
diff --git a/liquid-help-md/Cargo.toml b/liquid-help-md/Cargo.toml
new file mode 100644
index 0000000..d94de69
--- /dev/null
+++ b/liquid-help-md/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "liquid-help-md"
+version = "0.19.0"
+authors = ["Ed Page <eopage@gmail.com>"]
+description = "Describe Liquid language via markdown"
+repository = "https://github.com/cobalt-org/liquid-rust"
+documentation = "https://docs.rs/liquid"
+readme = "README.md"
+categories = ["template-engine"]
+keywords = ["liquid", "template", "templating", "language", "html"]
+license = "MIT OR Apache-2.0"
+
+[badges]
+azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
+
+[dependencies]
+liquid = { version = "0.19", path = "../" }
diff --git a/liquid-help-md/LICENSE-APACHE b/liquid-help-md/LICENSE-APACHE
new file mode 100644
index 0000000..8f71f43
--- /dev/null
+++ b/liquid-help-md/LICENSE-APACHE
@@ -0,0 +1,202 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   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.
+
diff --git a/liquid-help-md/LICENSE-MIT b/liquid-help-md/LICENSE-MIT
new file mode 100644
index 0000000..22b8d4e
--- /dev/null
+++ b/liquid-help-md/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) 2015 The assert_cli Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/liquid-help-md/README.md b/liquid-help-md/README.md
new file mode 100644
index 0000000..699604b
--- /dev/null
+++ b/liquid-help-md/README.md
@@ -0,0 +1,24 @@
+liquid-help-md
+===========
+
+> [Liquid templating](http://liquidmarkup.org/) for Rust
+
+[![Build Status](https://dev.azure.com/cobalt-org/cobalt-org/_apis/build/status/liquid-rust?branchName=master)](https://dev.azure.com/cobalt-org/cobalt-org/_build/latest?definitionId=1&branchName=master)
+[![Crates Status](https://img.shields.io/crates/v/liquid.svg)](https://crates.io/crates/liquid)
+[![Dependency Status](https://dependencyci.com/github/cobalt-org/liquid-rust/badge)](https://dependencyci.com/github/cobalt-org/liquid-rust)
+
+Usage
+----------
+
+To include liquid in your project add the following to your Cargo.toml:
+
+```toml
+[dependencies]
+liquid-help-md = "0.16"
+```
+
+Now you can use the crate in your code:
+
+```rust
+extern crate liquid_help_md;
+```
diff --git a/liquid-help-md/src/lib.rs b/liquid-help-md/src/lib.rs
new file mode 100644
index 0000000..1a7e0cf
--- /dev/null
+++ b/liquid-help-md/src/lib.rs
@@ -0,0 +1,108 @@
+pub trait Renderable {
+    fn render_summary(
+        &self,
+        stream: &mut dyn std::io::Write,
+    ) -> Result<(), Box<dyn std::error::Error>>;
+
+    fn render(&self, stream: &mut dyn std::io::Write) -> Result<(), Box<dyn std::error::Error>>;
+}
+
+impl Renderable for liquid::compiler::FilterReflection {
+    fn render_summary(
+        &self,
+        stream: &mut dyn std::io::Write,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.name(), self.description())?;
+        Ok(())
+    }
+
+    fn render(&self, stream: &mut dyn std::io::Write) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.name(), self.description())?;
+        writeln!(stream, "")?;
+        let params = self.positional_parameters();
+        if !params.is_empty() {
+            writeln!(stream, "Parameters (positional):")?;
+            writeln!(stream, "| Name | Description | Required? |")?;
+            writeln!(stream, "|------|-------------|-----------|")?;
+            for param in params {
+                writeln!(
+                    stream,
+                    "| {} | {} | {} |",
+                    param.name,
+                    param.description,
+                    if param.is_optional { "no" } else { "yes" }
+                )?;
+            }
+            writeln!(stream, "")?;
+        }
+        let params = self.keyword_parameters();
+        if !params.is_empty() {
+            writeln!(stream, "Parameters (named):")?;
+            writeln!(stream, "| Name | Description | Required? |")?;
+            writeln!(stream, "|------|-------------|-----------|")?;
+            for param in params {
+                writeln!(
+                    stream,
+                    "| {} | {} | {} |",
+                    param.name,
+                    param.description,
+                    if param.is_optional { "no" } else { "yes" }
+                )?;
+            }
+            writeln!(stream, "")?;
+        }
+        Ok(())
+    }
+}
+
+impl Renderable for liquid::compiler::TagReflection {
+    fn render_summary(
+        &self,
+        stream: &mut dyn std::io::Write,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.tag(), self.description())?;
+        Ok(())
+    }
+
+    fn render(&self, stream: &mut dyn std::io::Write) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.tag(), self.description())?;
+        writeln!(stream, "")?;
+        if let Some(spec) = self.spec() {
+            writeln!(stream, "Grammar: `{}`", spec)?;
+            writeln!(stream, "")?;
+        }
+        if let Some(example) = self.example() {
+            writeln!(stream, "Example:")?;
+            writeln!(stream, "```liquid")?;
+            writeln!(stream, "{}", example)?;
+            writeln!(stream, "```")?;
+        }
+        Ok(())
+    }
+}
+
+impl Renderable for liquid::compiler::BlockReflection {
+    fn render_summary(
+        &self,
+        stream: &mut dyn std::io::Write,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.start_tag(), self.description())?;
+        Ok(())
+    }
+
+    fn render(&self, stream: &mut dyn std::io::Write) -> Result<(), Box<dyn std::error::Error>> {
+        writeln!(stream, "**{}**: {}", self.start_tag(), self.description())?;
+        writeln!(stream, "")?;
+        if let Some(spec) = self.spec() {
+            writeln!(stream, "Grammar: `{}`", spec)?;
+            writeln!(stream, "")?;
+        }
+        if let Some(example) = self.example() {
+            writeln!(stream, "Example:")?;
+            writeln!(stream, "```liquid")?;
+            writeln!(stream, "{}", example)?;
+            writeln!(stream, "```")?;
+        }
+        Ok(())
+    }
+}
diff --git a/src/bin/liquid-dbg/main.rs b/src/bin/liquid-dbg/main.rs
deleted file mode 100644
index 24b4870..0000000
--- a/src/bin/liquid-dbg/main.rs
+++ /dev/null
@@ -1,122 +0,0 @@
-// Allow zero pointers for lazy_static. Otherwise clippy will complain.
-#![allow(unknown_lints)]
-
-#[macro_use]
-extern crate clap;
-#[macro_use]
-extern crate error_chain;
-extern crate liquid;
-
-#[cfg(feature = "serde_json")]
-extern crate serde_json;
-#[cfg(feature = "serde_yaml")]
-extern crate serde_yaml;
-
-use std::ffi;
-use std::fs;
-use std::io;
-use std::io::Write;
-use std::path;
-
-error_chain! {
-    links {
-    }
-
-    foreign_links {
-        Clap(clap::Error);
-        Io(io::Error);
-        Liquid(liquid::Error);
-        Yaml(serde_yaml::Error) #[cfg(feature = "serde_yaml")];
-        Json(serde_json::Error) #[cfg(feature = "serde_json")];
-    }
-
-    errors {
-    }
-}
-
-fn option<'a>(name: &'a str, value: &'a str) -> clap::Arg<'a, 'a> {
-    clap::Arg::with_name(name).long(name).value_name(value)
-}
-
-#[cfg(feature = "serde_yaml")]
-fn load_yaml(path: &path::Path) -> Result<liquid::value::Value> {
-    let f = fs::File::open(path)?;
-    serde_yaml::from_reader(f).map_err(|e| e.into())
-}
-
-#[cfg(not(feature = "serde_yaml"))]
-fn load_yaml(_path: &path::Path) -> Result<liquid::value::Value> {
-    bail!("yaml is unsupported");
-}
-
-#[cfg(feature = "serde_json")]
-fn load_json(path: &path::Path) -> Result<liquid::value::Value> {
-    let f = fs::File::open(path)?;
-    serde_json::from_reader(f).map_err(|e| e.into())
-}
-
-#[cfg(not(feature = "serde_json"))]
-fn load_json(_path: &path::Path) -> Result<liquid::value::Value> {
-    bail!("json is unsupported");
-}
-
-fn build_context(path: &path::Path) -> Result<liquid::value::Object> {
-    let extension = path.extension().unwrap_or_else(|| ffi::OsStr::new(""));
-    let value = if extension == ffi::OsStr::new("yaml") {
-        load_yaml(path)
-    } else if extension == ffi::OsStr::new("yaml") {
-        load_json(path)
-    } else {
-        Err("Unsupported file type".into())
-    }?;
-    let value = match value {
-        liquid::value::Value::Object(o) => Ok(o),
-        _ => Err("File must be an object"),
-    }?;
-
-    Ok(value)
-}
-
-fn run() -> Result<()> {
-    let matches = clap::App::new("liquidate")
-        .version(crate_version!())
-        .author(crate_authors!())
-        .arg(option("input", "LIQUID").required(true))
-        .arg(option("output", "TXT"))
-        .arg(option("context", "YAML"))
-        .get_matches_safe()?;
-
-    let parser = liquid::ParserBuilder::with_liquid()
-        .extra_filters()
-        .jekyll_filters()
-        .build()
-        .expect("should succeed without partials");
-    let template_path = matches
-        .value_of("input")
-        .map(path::PathBuf::from)
-        .expect("Parameter was required");
-    let template = parser.parse_file(template_path)?;
-
-    let data = matches
-        .value_of("context")
-        .map(|s| {
-            let p = path::PathBuf::from(s);
-            build_context(p.as_path())
-        })
-        .map_or(Ok(None), |r| r.map(Some))?
-        .unwrap_or_else(liquid::value::Object::new);
-    let output = template.render(&data)?;
-    match matches.value_of("output") {
-        Some(path) => {
-            let mut out = fs::File::create(path::PathBuf::from(path))?;
-            out.write_all(output.as_bytes())?;
-        }
-        None => {
-            println!("{}", output);
-        }
-    }
-
-    Ok(())
-}
-
-quick_main!(run);
diff --git a/src/lib.rs b/src/lib.rs
index f43d02e..fcda7f0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -41,6 +41,7 @@ extern crate liquid_interpreter;
 extern crate liquid_value;
 
 mod parser;
+mod reflection;
 mod template;
 
 /// Allows `liquid-derive` macros to work inside this crate.
@@ -74,6 +75,7 @@ pub mod tags;
 pub use interpreter::ValueStore;
 pub use liquid_error::Error;
 pub use parser::*;
+pub use reflection::*;
 pub use template::*;
 
 #[macro_use]
diff --git a/src/parser.rs b/src/parser.rs
index 1958fa9..3a81121 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -10,6 +10,7 @@ use liquid_interpreter as interpreter;
 use super::Template;
 use filters;
 use partials;
+use reflection;
 use tags;
 
 /// Storage for partial-templates.
@@ -155,21 +156,21 @@ where
     /// Inserts a new custom block into the parser
     pub fn block<B: Into<Box<dyn compiler::ParseBlock>>>(mut self, block: B) -> Self {
         let block = block.into();
-        self.blocks.register(block.start_tag(), block);
+        self.blocks.register(block.reflection().start_tag(), block);
         self
     }
 
     /// Inserts a new custom tag into the parser
     pub fn tag<T: Into<Box<dyn compiler::ParseTag>>>(mut self, tag: T) -> Self {
         let tag = tag.into();
-        self.tags.register(tag.tag(), tag);
+        self.tags.register(tag.reflection().tag(), tag);
         self
     }
 
     /// Inserts a new custom filter into the parser
     pub fn filter<F: Into<Box<dyn compiler::ParseFilter>>>(mut self, filter: F) -> Self {
         let filter = filter.into();
-        self.filters.register(filter.name(), filter);
+        self.filters.register(filter.reflection().name(), filter);
         self
     }
 
@@ -226,6 +227,32 @@ where
     }
 }
 
+impl<P> reflection::ParserReflection for ParserBuilder<P>
+where
+    P: partials::PartialCompiler,
+{
+    fn blocks<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::BlockReflection> + 'r> {
+        Box::new(self.blocks.plugins().map(|p| p.reflection()))
+    }
+
+    fn tags<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::TagReflection> + 'r> {
+        Box::new(self.tags.plugins().map(|p| p.reflection()))
+    }
+
+    fn filters<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::FilterReflection> + 'r> {
+        Box::new(self.filters.plugins().map(|p| p.reflection()))
+    }
+
+    fn partials<'r>(&'r self) -> Box<Iterator<Item = &str> + 'r> {
+        Box::new(
+            self.partials
+                .as_ref()
+                .into_iter()
+                .flat_map(|s| s.source().names()),
+        )
+    }
+}
+
 #[derive(Default, Clone)]
 pub struct Parser {
     options: sync::Arc<compiler::Language>,
@@ -302,3 +329,21 @@ impl Parser {
         self.parse(&buf)
     }
 }
+
+impl reflection::ParserReflection for Parser {
+    fn blocks<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::BlockReflection> + 'r> {
+        Box::new(self.options.blocks.plugins().map(|p| p.reflection()))
+    }
+
+    fn tags<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::TagReflection> + 'r> {
+        Box::new(self.options.tags.plugins().map(|p| p.reflection()))
+    }
+
+    fn filters<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::FilterReflection> + 'r> {
+        Box::new(self.options.filters.plugins().map(|p| p.reflection()))
+    }
+
+    fn partials<'r>(&'r self) -> Box<Iterator<Item = &str> + 'r> {
+        Box::new(self.partials.as_ref().into_iter().flat_map(|s| s.names()))
+    }
+}
diff --git a/src/partials/eager.rs b/src/partials/eager.rs
index c480949..dbb470b 100644
--- a/src/partials/eager.rs
+++ b/src/partials/eager.rs
@@ -102,6 +102,10 @@ where
         let store = EagerStore { store };
         Ok(Box::new(store))
     }
+
+    fn source(&self) -> &dyn PartialSource {
+        &self.source
+    }
 }
 
 struct EagerStore {
diff --git a/src/partials/lazy.rs b/src/partials/lazy.rs
index 75ef1b2..f728ef8 100644
--- a/src/partials/lazy.rs
+++ b/src/partials/lazy.rs
@@ -88,6 +88,10 @@ where
         };
         Ok(Box::new(store))
     }
+
+    fn source(&self) -> &dyn PartialSource {
+        &self.source
+    }
 }
 
 struct LazyStore<S: PartialSource> {
diff --git a/src/partials/mod.rs b/src/partials/mod.rs
index b833f5e..9f4e039 100644
--- a/src/partials/mod.rs
+++ b/src/partials/mod.rs
@@ -27,6 +27,9 @@ pub use self::ondemand::*;
 pub trait PartialCompiler {
     /// Convert a `PartialSource` into a `PartialStore`.
     fn compile(self, language: sync::Arc<Language>) -> Result<Box<dyn PartialStore + Send + Sync>>;
+
+    /// Access underlying `PartialSource`
+    fn source(&self) -> &dyn PartialSource;
 }
 
 /// Partial-template source repository.
diff --git a/src/partials/ondemand.rs b/src/partials/ondemand.rs
index 1b2a54b..96f633d 100644
--- a/src/partials/ondemand.rs
+++ b/src/partials/ondemand.rs
@@ -85,6 +85,10 @@ where
         };
         Ok(Box::new(store))
     }
+
+    fn source(&self) -> &dyn PartialSource {
+        &self.source
+    }
 }
 
 struct OnDemandStore<S: PartialSource> {
diff --git a/src/reflection.rs b/src/reflection.rs
new file mode 100644
index 0000000..8f7bb50
--- /dev/null
+++ b/src/reflection.rs
@@ -0,0 +1,11 @@
+use liquid_compiler as compiler;
+
+pub trait ParserReflection {
+    fn blocks<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::BlockReflection> + 'r>;
+
+    fn tags<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::TagReflection> + 'r>;
+
+    fn filters<'r>(&'r self) -> Box<Iterator<Item = &dyn compiler::FilterReflection> + 'r>;
+
+    fn partials<'r>(&'r self) -> Box<Iterator<Item = &str> + 'r>;
+}
diff --git a/src/tags/assign_tag.rs b/src/tags/assign_tag.rs
index cfc2632..9834e33 100644
--- a/src/tags/assign_tag.rs
+++ b/src/tags/assign_tag.rs
@@ -80,6 +80,10 @@ impl ParseTag for AssignTag {
 
         Ok(Box::new(Assign { dst, src }))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/capture_block.rs b/src/tags/capture_block.rs
index c08eb57..6e9c213 100644
--- a/src/tags/capture_block.rs
+++ b/src/tags/capture_block.rs
@@ -87,6 +87,10 @@ impl ParseBlock for CaptureBlock {
         tokens.assert_empty();
         Ok(Box::new(Capture { id, template }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/case_block.rs b/src/tags/case_block.rs
index cf4b682..5fde2e6 100644
--- a/src/tags/case_block.rs
+++ b/src/tags/case_block.rs
@@ -189,6 +189,10 @@ impl ParseBlock for CaseBlock {
             else_block,
         }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/comment_block.rs b/src/tags/comment_block.rs
index b803851..0ed2687 100644
--- a/src/tags/comment_block.rs
+++ b/src/tags/comment_block.rs
@@ -70,6 +70,10 @@ impl ParseBlock for CommentBlock {
         tokens.assert_empty();
         Ok(Box::new(Comment))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/cycle_tag.rs b/src/tags/cycle_tag.rs
index 6d2f4d8..2345f97 100644
--- a/src/tags/cycle_tag.rs
+++ b/src/tags/cycle_tag.rs
@@ -125,6 +125,10 @@ impl ParseTag for CycleTag {
     fn parse(&self, arguments: TagTokenIter, options: &Language) -> Result<Box<dyn Renderable>> {
         parse_cycle(arguments, options).map(|opt| Box::new(opt) as Box<dyn Renderable>)
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Default)]
diff --git a/src/tags/for_block.rs b/src/tags/for_block.rs
index 77999c1..da6ef24 100644
--- a/src/tags/for_block.rs
+++ b/src/tags/for_block.rs
@@ -337,6 +337,10 @@ impl ParseBlock for ForBlock {
             reversed,
         }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 #[derive(Debug)]
@@ -537,6 +541,10 @@ impl ParseBlock for TableRowBlock {
             offset,
         }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 /// Format an error for an unexpected value.
diff --git a/src/tags/if_block.rs b/src/tags/if_block.rs
index 7f931e4..2000d77 100644
--- a/src/tags/if_block.rs
+++ b/src/tags/if_block.rs
@@ -359,6 +359,10 @@ impl ParseBlock for UnlessBlock {
             if_false,
         }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 fn parse_if(
@@ -436,6 +440,10 @@ impl ParseBlock for IfBlock {
         tokens.assert_empty();
         Ok(conditional)
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 /// Format an error for an unexpected value.
diff --git a/src/tags/ifchanged_block.rs b/src/tags/ifchanged_block.rs
index 54d2073..8c94cd1 100644
--- a/src/tags/ifchanged_block.rs
+++ b/src/tags/ifchanged_block.rs
@@ -76,6 +76,10 @@ impl ParseBlock for IfChangedBlock {
         tokens.assert_empty();
         Ok(Box::new(IfChanged { if_changed }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 /// Remembers the content of the last rendered `ifstate` block.
diff --git a/src/tags/include_tag.rs b/src/tags/include_tag.rs
index ed66bbd..8671a8f 100644
--- a/src/tags/include_tag.rs
+++ b/src/tags/include_tag.rs
@@ -77,6 +77,10 @@ impl ParseTag for IncludeTag {
 
         Ok(Box::new(Include { partial }))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/increment_tags.rs b/src/tags/increment_tags.rs
index 133ff62..a7750d3 100644
--- a/src/tags/increment_tags.rs
+++ b/src/tags/increment_tags.rs
@@ -69,6 +69,10 @@ impl ParseTag for IncrementTag {
 
         Ok(Box::new(Increment { id }))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[derive(Clone, Debug)]
@@ -130,6 +134,10 @@ impl ParseTag for DecrementTag {
 
         Ok(Box::new(Decrement { id }))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/interrupt_tags.rs b/src/tags/interrupt_tags.rs
index e5a046a..48cba41 100644
--- a/src/tags/interrupt_tags.rs
+++ b/src/tags/interrupt_tags.rs
@@ -48,6 +48,10 @@ impl ParseTag for BreakTag {
         arguments.expect_nothing()?;
         Ok(Box::new(Break))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[derive(Copy, Clone, Debug)]
@@ -89,6 +93,10 @@ impl ParseTag for ContinueTag {
         arguments.expect_nothing()?;
         Ok(Box::new(Continue))
     }
+
+    fn reflection(&self) -> &dyn TagReflection {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/src/tags/raw_block.rs b/src/tags/raw_block.rs
index 7a605a1..650b42a 100644
--- a/src/tags/raw_block.rs
+++ b/src/tags/raw_block.rs
@@ -60,6 +60,10 @@ impl ParseBlock for RawBlock {
         tokens.assert_empty();
         Ok(Box::new(RawT { content }))
     }
+
+    fn reflection(&self) -> &dyn BlockReflection {
+        self
+    }
 }
 
 #[cfg(test)]


Test patch:
diff --git a/tests/derive_macros_test_filters/stateful.rs b/tests/derive_macros_test_filters/stateful.rs
index e37b9b4..3b0eaad 100644
--- a/tests/derive_macros_test_filters/stateful.rs
+++ b/tests/derive_macros_test_filters/stateful.rs
@@ -52,6 +52,10 @@ impl ParseFilter for TestStatefulFilterParser {
 
         Ok(Box::new(TestStatefulFilter { args, state }))
     }
+
+    fn reflection(&self) -> &dyn liquid::compiler::FilterReflection {
+        self
+    }
 }
 
 #[derive(Debug, Display_filter)]