diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 00000000..bbb920b8
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,29 @@
+name: Docs - Github Pages
+on:
+ push:
+ branches:
+ # - main
+ - release-2.0
+ - jh/2.0-docs
+ paths:
+ - docs/**
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup mdBook
+ uses: peaceiris/actions-mdbook@v2
+ with:
+ mdbook-version: "latest"
+
+ - run: mdbook build
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./docs-dist
+ publish_branch: docs
diff --git a/.gitignore b/.gitignore
index 5e5107fd..e5e6f26b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ spec/dummy/db/*.sqlite3-journal
spec/dummy/log/*.log
spec/dummy/tmp/
doc/
+docs-dist/
.yardoc/
Gemfile.lock
.vscode
diff --git a/book.toml b/book.toml
new file mode 100644
index 00000000..28bc0b94
--- /dev/null
+++ b/book.toml
@@ -0,0 +1,16 @@
+[book]
+authors = ["Procore Technologies, Inc."]
+language = "en"
+src = "docs"
+title = "Blueprinter"
+
+[build]
+build-dir = "docs-dist"
+
+# https://rust-lang.github.io/mdBook/format/configuration/renderers.html?highlight=top%20bar#html-renderer-options
+[output.html]
+additional-css = ["theme/hljs-overrides.css", "theme/custom.css"]
+default-theme = "light"
+preferred-dark-theme = "navy"
+no-section-label = true
+git-repository-url = "https://github.com/procore-oss/blueprinter"
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
new file mode 100644
index 00000000..0629fba6
--- /dev/null
+++ b/docs/SUMMARY.md
@@ -0,0 +1,30 @@
+# Summary
+
+- [Introduction](./introduction.md)
+
+- [Blueprinter DSL](./dsl/index.md)
+ - [Fields](./dsl/fields.md)
+ - [Views](./dsl/views.md)
+ - [Partials](./dsl/partials.md)
+ - [Formatters](./dsl/formatters.md)
+ - [Options](./dsl/options.md)
+ - [Extensions](./dsl/extensions.md)
+
+- [Rendering](./rendering.md)
+
+- [Blueprinter API](./api/index.md)
+ - [Extensions](./api/extensions.md)
+ - [Reflection](./api/reflection.md)
+ - [Context Objects](./api/context-objects.md)
+ - [Fields](./api/fields.md)
+
+---
+
+- [Upgrading](./upgrading/index.md)
+ - [Configuration](./upgrading/configuration.md)
+ - [Customization](./upgrading/customization.md)
+ - [Fields](./upgrading/fields.md)
+ - [Rendering](./upgrading/rendering.md)
+ - [Reflection](./upgrading/reflection.md)
+ - [Extensions](./upgrading/extensions.md)
+- [Legacy/V1 Docs](./v1.md)
diff --git a/docs/api/context-objects.md b/docs/api/context-objects.md
new file mode 100644
index 00000000..ee74336d
--- /dev/null
+++ b/docs/api/context-objects.md
@@ -0,0 +1,112 @@
+# Context Objects
+
+Context objects are the arguments passed to APIs like [field blocks](../dsl/fields.md#field-blocks), [option procs](../dsl/options.md) and [extension hooks](./extensions.md). There are several kinds of context objects, each with its own set of fields.
+
+## Render Context
+
+_* Field can be assigned._
+
+> **blueprint** \
+> The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
+
+> **fields** * \
+> A frozen array of field definitions that will be serialized, in order. See [Fields API](./fields.md).
+
+> **options** * \
+> The frozen options Hash passed to `render`. An empty Hash if none was passed.
+
+> **store** \
+> A Hash that can be used to store & access information by extensions and your application.
+
+> **depth** \
+> The current blueprint depth (1-indexed).
+
+## Object Context
+
+_* Field can be assigned._
+
+> **blueprint** \
+> The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
+
+> **fields** \
+> A frozen array of field definitions that will be serialized, in order. See [Fields API](./fields.md) and the [blueprint_fields](./extensions.md#blueprint_fields) hook.
+
+> **options** \
+> The frozen options Hash passed to `render`. An empty Hash if none was passed.
+
+> **object** * \
+> The object or collection currently being serialized.
+
+> **store** \
+> A Hash that can be used to store & access information by extensions and your application.
+
+> **depth** \
+> The current blueprint depth (1-indexed).
+
+## Field Context
+
+> **blueprint** \
+> The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
+
+> **fields** \
+> A frozen array of field definitions that will be serialized, in order. See [Fields API](./fields.md) and the [blueprint_fields](./extensions.md#blueprint_fields) hook.
+
+> **options** \
+> The frozen options Hash passed to `render`. An empty Hash if none was passed.
+
+> **object** \
+> The object currently being serialized.
+
+> **field** \
+> A struct of the field, object, or collection currently being rendered. You can use this to access the field's name and options. See [Fields API](./fields.md).
+
+> **store** \
+> A Hash that can be used to store & access information by extensions and your application.
+
+> **depth** \
+> The current blueprint depth (1-indexed).
+
+## Result Context
+
+_* Field can be assigned._
+
+> **blueprint** * \
+> The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
+
+> **fields** \
+> A frozen array of field definitions that were serialized, in order. See [Fields API](./fields.md) and the [around_blueprint_init](./extensions.md#around_blueprint_init) hook.
+
+> **options** * \
+> The frozen options Hash passed to `render`. An empty Hash if none was passed.
+
+> **object** * \
+> The object or collection that was just serialized.
+
+> **store** \
+> A Hash that can be used to store & access information by extensions and your application.
+
+> **format** * \
+> The requested serialization format (e.g. `:json`, `:hash`).
+
+## Hook Context
+
+> **blueprint** \
+> The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
+
+> **fields** \
+> A frozen array of field definitions that will be serialized, in order. See [Fields API](./fields.md) and the [around_blueprint_init](./extensions.md#around_blueprint_init) hook.
+
+> **options** \
+> The frozen options Hash passed to `render`. An empty Hash if none was passed.
+
+> **extension** \
+> Instance of the current extension
+
+> **hook** \
+> Name of the current hook
+
+> **store** \
+> A Hash that can be used to store & access information by extensions and your application.
+
+> **depth** \
+> The current blueprint depth (1-indexed).
diff --git a/docs/api/extensions.md b/docs/api/extensions.md
new file mode 100644
index 00000000..924b04ed
--- /dev/null
+++ b/docs/api/extensions.md
@@ -0,0 +1,258 @@
+# Extensions
+
+Blueprinter has a powerful middleware-based extension system with hooks for every step of the serialization lifecycle. In fact, many of Blueprinter's features are implemented using the extension API!
+
+Simply extend the `Blueprinter::Extension` class, define the hooks you need, and [add it to your configuration](../dsl/extensions.md#using-extensions).
+
+## Hooks
+
+Hooks are called in the following order. They are passed a [context object](./context-objects.md) as an argument.
+
+
+
+Additionally, the [around_hook](#around_hook) hook runs around all other hooks.
+
+### around_result
+
+> **param** [Result Context](./context-objects.md#result-context) \
+> **return** result \
+> **cost** Low - run once during render
+
+The `around_result` hook runs around the entire serialization process, allowing you to modify the initial input and final output.
+
+The following example hook caches an entire result for five minutes.
+
+```ruby
+def around_result(ctx)
+ cache(ctx.blueprint.class, ctx.object, ctx.format, ttl: 300) do
+ yield ctx
+ end
+end
+```
+
+#### Finalizing
+
+The `final` and `final?` helpers allow middleware to declare, and check if, a result is "finalized" and should no longer be altered. These helpers should **only** be used in `around_result`.
+
+```ruby
+def around_result(ctx)
+ result = yield ctx
+ return result if final? result
+
+ result = somehow_modify result
+ final result
+end
+```
+
+### around_blueprint_init
+
+> **param** [Render Context](./context-objects.md#render-context) \
+> **cost** Low - run once per used blueprint during render
+
+The `around_blueprint_init` hook runs the first time a new Blueprint is used during a render cycle. It can be used by extensions to perform time-saving setup before a render.
+
+```ruby
+def around_blueprint_init(ctx)
+ perform_setup ctx.blueprint, ctx.options
+ yield ctx
+end
+```
+
+`around_blueprint_init` MUST yield, otherwise a `Blueprinter::Errors::ExtensionHook` will be raised.
+
+### around_serialize_object
+
+> **param** [Object Context](./context-objects.md#object-context) \
+> **return** result \
+> **cost** Medium - run every time any blueprint is rendered
+
+The `around_serialize_object` hook runs around every object (as opposed to collection) that's serialized. The following example would see it called **four** times: once for the category itself and once for each item.
+
+```ruby
+CategoryBlueprint.render({
+ name: "Foo",
+ items: [item1, item2, item3],
+}).to_json
+```
+
+The following example hook modifies both the input object and the output result.
+
+```ruby
+def around_serialize_object(ctx)
+ # modify the object before it's serialized
+ ctx.object = modify ctx.object
+
+ result = yield ctx
+
+ # modify the result
+ result.merge({ foo: "Bar" })
+end
+```
+
+### around_serialize_collection
+
+> **param** [Object Context](./context-objects.md#object-context) \
+> **return** result \
+> **cost** Medium - run every time any blueprint is rendered
+
+The `around_serialize_collection` hook runs around every collection that's serialized. The following example would see it called three times: once for the array of categories and once for each set of items.
+
+```ruby
+CategoryBlueprint.render([
+ { name: "Foo", items: [item1] },
+ { name: "Bar", items: [item2, item3] },
+]).to_json
+```
+
+The following example hook modifies both the input collection and the output results.
+
+```ruby
+def around_serialize_collection(ctx)
+ # modify the collection before it's serialized
+ ctx.object = modify ctx.object
+
+ result = yield ctx
+
+ # modify the result
+ result.reject { |obj| some_logic obj }
+end
+```
+
+### around_blueprint
+
+> **param** [Object Context](./context-objects.md#object-context) \
+> **return** result \
+> **cost** Medium - run every time any blueprint is rendered
+
+The `around_blueprint` hook runs every time an object, including members of collections, are serialized. The following example would see it called three times: once for the category and once for each item.
+
+```ruby
+CategoryBlueprint.render({ name: "Foo", items: [item1, item] }).to_json
+```
+
+The following example hook modifies both the input object and the output result.
+
+```ruby
+def around_blueprint(ctx)
+ # modify the object before it's serialized
+ ctx.object = modify ctx.object
+
+ result = yield ctx
+
+ # modify the result
+ result.merge({ foo: "Bar" })
+end
+```
+
+### around_field_value
+
+> **param** [Field Context](./context-objects.md#field-context) \
+> **return** result \
+> **cost** High - run for every (non-object, non-collection) field
+
+The `around_field_value` hook runs around every non-object, non-collection field that's extracted. The following example trims leading and trailing whitespace from string values.
+
+```ruby
+def around_field_value(ctx)
+ val = yield ctx
+ case val
+ when String then val.strip
+ else val
+ end
+end
+```
+
+#### Skipping fields
+
+You can tell blueprinter to completely skip a field using `skip`. It will bail out of the hook and any previous hooks that yielded.
+
+```ruby
+def around_field_value(ctx)
+ val = yield ctx
+ skip if ctx.field.options[:skip_on] == val
+ val
+end
+```
+
+### around_object_value
+
+> **param** [Field Context](./context-objects.md#field-context) \
+> **return** result \
+> **cost** High - run for every object field
+
+The `around_object_value` hook runs around every object field that's extracted (before it's serialized with a Blueprint). The following example adds a `foo` attribute to each object Hash.
+
+```ruby
+def around_object_value(ctx)
+ val = yield ctx
+ case val
+ when Hash then val.merge({ foo: "bar" })
+ else val
+ end
+end
+```
+
+#### Skipping fields
+
+You can tell blueprinter to completely skip a field using `skip`. It will bail out of the hook and any previous hooks that yielded.
+
+```ruby
+def around_object_value(ctx)
+ val = yield ctx
+ skip if ctx.field.options[:skip_on] == val
+ val
+end
+```
+
+### around_collection_value
+
+> **param** [Field Context](./context-objects.md#field-context) \
+> **return** result \
+> **cost** High - run for every collection field
+
+The `around_collection_value` hook runs around every collection field that's extracted (before it's serialized with a Blueprint). The following example removes deleted widgets from a collection.
+
+```ruby
+def around_collection_value(ctx)
+ val = yield ctx
+ case ctx.field.blueprint
+ when WidgetBlueprint
+ val.reject { |widget| widget.deleted? }
+ else
+ val
+ end
+end
+```
+
+#### Skipping fields
+
+You can tell blueprinter to completely skip a field using `skip`. It will bail out of the hook and any previous hooks that yielded.
+
+```ruby
+def around_collection_value(ctx)
+ val = yield ctx
+ skip if ctx.field.options[:skip_on] == val
+ val
+end
+```
+
+### around_hook
+
+> **param** [Hook Context](./context-objects.md#hook-context) \
+> **cost** Variable - runs around all your extensions
+
+The `around_hook` hook runs around all other extension hooks. It **must** yield, otherwise a `Blueprinter::Errors::ExtensionHook` will be raised. The return value from `yield` is **not** used, nor is the return value of `around_hook`.
+
+```ruby
+def around_hook(ctx)
+ # do something
+ yield
+ # do something else
+end
+```
diff --git a/docs/api/fields.md b/docs/api/fields.md
new file mode 100644
index 00000000..bac3355a
--- /dev/null
+++ b/docs/api/fields.md
@@ -0,0 +1,31 @@
+# Fields
+
+Extensions, reflection, and other APIs allow access to structs that describe fields.
+
+> **type**\
+> *Symbol* `:field | :object | :collection`\
+> The type of field.
+
+> **name**\
+> *Symbol*\
+> Name of the field as it will appear in the JSON or Hash output.
+
+> **from**\
+> *Symbol*\
+> Name of the field in the source object (usually the same as `name`).
+
+> **from_str**\
+> *String*\
+> Same as `from`, but as a frozen string.
+
+> **value_proc**\
+> *nil | Proc*\
+> The block passed to the field definition (if given). Expects a [Field Context](./context-objects.md#field-context) argument and returns the field value.
+
+> **options**\
+> *Hash*\
+> A frozen Hash of any additional options passed to the field.
+
+> **blueprint** (object and collection only)\
+> *Class*\
+> The blueprint to use for serializaing the object or collection.
diff --git a/docs/api/index.md b/docs/api/index.md
new file mode 100644
index 00000000..d03e7341
--- /dev/null
+++ b/docs/api/index.md
@@ -0,0 +1,19 @@
+# Blueprinter API
+
+Blueprinter has a rich API for extending the serialization process and reflecting on your blueprints.
+
+## Extensions
+
+The extensions API offers middleware for various parts of the serialization process. [Read more](./extensions.md).
+
+## Reflection
+
+The reflection API allows your application, or Blueprinter extensions, to introspect on your blueprints' options, fields, and views. [Read more](./reflection.md).
+
+## Context Objects
+
+Context objects are the arguments you'll receive in most of the above APIs. [Read more](./context-objects.md).
+
+## Fields
+
+Several APIs provide access to structs describing field definitions. [Read more](./fields.md).
diff --git a/docs/api/reflection.md b/docs/api/reflection.md
new file mode 100644
index 00000000..15458fec
--- /dev/null
+++ b/docs/api/reflection.md
@@ -0,0 +1,141 @@
+# Reflection
+
+Blueprints may be reflected on to inspect their views, fields, and options. This is useful for building [extensions](./extensions.md), and possibly even for some applications.
+
+We will use the following blueprint in the examples below:
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ field :description, exclude_if_empty: true
+ object :category, CategoryBlueprint
+ collection :parts, PartBlueprint
+
+ view :extended do
+ object :manufacturer, CompanyBlueprint[:full]
+
+ view :with_price do
+ field :price
+ end
+ end
+end
+```
+
+## Blueprint & view names
+
+```ruby
+WidgetBlueprint.blueprint_name
+=> "WidgetBlueprint"
+
+WidgetBlueprint.view_name
+=> :default
+
+WidgetBlueprint[:extended].blueprint_name
+=> "WidgetBlueprint.extended"
+
+WidgetBlueprint[:extended].view_name
+=> :extended
+
+WidgetBlueprint["extended.with_price"].blueprint_name
+=> "WidgetBlueprint.extended.with_price"
+
+WidgetBlueprint["extended.with_price"].view_name
+=> :"extended.with_price"
+```
+
+## Blueprint & view options
+
+```ruby
+WidgetBlueprint.options
+=> {exclude_if_nil: true}
+
+WidgetBlueprint[:extended].options
+=> {exclude_if_nil: true, exclude_if_empty: true}
+```
+
+## Views
+
+Here, `:default` refers to the top level of the blueprint.
+
+```ruby
+WidgetBlueprint.reflections.keys
+=> [:default, :extended, :"extended.with_price"]
+```
+
+You can also reflect directly on a view.
+
+```ruby
+WidgetBlueprint[:extended].reflections.keys
+=> [:default, :with_price]
+```
+
+**Notice that the names are relative**: `:default` now refers to the `:extended` view, since we called `.reflections` on `:extended`. The prefix is also gone from the nested `:with_price` view.
+
+## Fields
+
+```ruby
+view = WidgetBlueprint.reflections[:default]
+
+# Regular fields
+view.fields.keys
+=> [:name, :description]
+
+# Object fields
+view.objects.keys
+=> [:category]
+
+# Collection fields
+view.collections.keys
+=> [:parts]
+
+# All fields in the order they were defined
+view.ordered
+# returns an array of field objects
+```
+
+## Field metadata
+
+```ruby
+view = WidgetBlueprint.reflections[:default]
+field = view.fields[:description]
+
+field.name
+=> :description
+
+field.from
+=> :description # the :from option in the DSL
+
+field.value_proc
+=> nil # the block you passed to the field, if any
+
+field.options # all other options passed to the field
+=> { exclude_if_empty: true }
+```
+
+Object and collection fields have the same metadata as regular fields, plus a `blueprint` attribute:
+
+```ruby
+view = WidgetBlueprint.reflections[:default]
+field = view.collections[:parts]
+
+# it returns the Blueprint class, so you can continue reflecting
+field.blueprint
+=> PartBlueprint
+
+field.blueprint.reflections[:default].fields
+=> # array of fields on the default view of PartBlueprint
+```
+
+If you used a view in an object or collection field, you can reflect on that view just like a blueprint:
+
+```ruby
+view = WidgetBlueprint.reflections[:extended]
+field = view.objects[:manufacturer]
+
+field.blueprint.to_s
+=> "CompanyBlueprint.full"
+
+# Remember, we're reflecting ON the :full view, so the name is relative!
+field.blueprint.reflections[:default].fields
+=> # array of fields on the :full view of CompanyBlueprint
+```
diff --git a/docs/dsl/extensions.md b/docs/dsl/extensions.md
new file mode 100644
index 00000000..9d647d8c
--- /dev/null
+++ b/docs/dsl/extensions.md
@@ -0,0 +1,97 @@
+# Extensions
+
+Blueprinter has a powerful extension system that permits middleware throughout the entire serialization lifecycle. Some extensions are included with Blueprinter, others are available as gems, and you can easily write your own using the [Extension API](../api/extensions.md).
+
+## Using extensions
+
+Extensions can be added to your `ApplicationBlueprint` or any other blueprint, view, or partial. They're inherited from parent classes and views, but can be overridden.
+
+```ruby
+class MyBlueprint < ApplicationBlueprint
+ # This extension instance will exist for the duration of your program
+ extensions << FooExtension.new
+
+ # These extensions will be initialized once during each render
+ extensions << BarExtension
+ extensions << -> { ZorpExtension.new(some_args) }
+
+ # Inline extensions are also initialized once per render
+ extension do
+ def around_blueprint(ctx)
+ result = yield ctx
+ result.merge({ foo: "Foo" })
+ end
+ end
+
+ view :minimal do
+ # extensions is a simple Array, so you can add or remove elements
+ extensions.select! { |ext| ext.is_a? FooExtension }
+
+ # or simply replace the whole Array
+ self.extensions = [FooExtension.new]
+ end
+end
+```
+
+## Included extensions
+
+These extensions are distributed with Blueprinter. Simply add them to your configuration.
+
+### Field Order
+
+Control the order of fields in your output. See [Fields API](../api/fields.md) for more information about the block parameters.
+
+```ruby
+extensions << Blueprinter::Extensions::FieldOrder.new { |a, b| a.name <=> b.name }
+```
+
+### MultiJson
+
+The MultiJson extension switches Blueprinter from Ruby's built-in JSON library to the [multi_json](https://rubygems.org/gems/multi_json) gem. Just install the `multi_json` gem, your serialization library of choice, and enable the extension.
+
+```ruby
+extensions << Blueprinter::Extensions::MultiJson.new
+
+# Any options you pass will be forwarded to MultiJson.dump
+extensions << Blueprinter::Extensions::MultiJson.new(pretty: true)
+
+# You can also pass MultiJson.dump options during render
+WidgetBlueprint.render(widget, multi_json: { pretty: true }).to_json
+```
+
+If `multi_json` doesn't support your preferred JSON library, you can use Blueprinter's [around_result](../api/extensions.md#around_result) extension hook to render JSON however you like.
+
+### OpenTelemetry
+
+Enable the OpenTelemetry extension to see what's happening while you render your blueprints. One outer `blueprinter.render` span will nest various `blueprinter.object` and `blueprinter.collection` spans. Each span will include the blueprint/view name that triggered it.
+
+Extension hooks will be wrapped in `blueprinter.extension` spans and annotated with the current extension and hook name.
+
+```ruby
+extensions << Blueprinter::Extensions::OpenTelemetry.new("my-tracer-name")
+```
+
+### ViewOption
+
+The ViewOption extension uses the [around_result](../api/extensions.md#around_result) extension hook to add a `view` option to `render`, `render_object`, and `render_collection`. It allows V1-compatible rendering of views.
+
+```ruby
+extensions << Blueprinter::Extensions::ViewOption.new
+```
+
+Now you can render a view either way:
+
+```ruby
+# V2 style
+MyBlueprint[:foo].render(obj)
+# or V1 style
+MyBlueprint.render(obj, view: :foo)
+```
+
+## Gem extensions
+
+_Have an extension you'd like to share? Let us know and we may add it to the list!_
+
+### blueprinter-activerecord
+
+[blueprinter-activerecord](https://github.com/procore-oss/blueprinter-activerecord) is an official extension from the Blueprinter team providing ActiveRecord integration, including automatic preloading of associations based on your Blueprint definitions.
diff --git a/docs/dsl/fields.md b/docs/dsl/fields.md
new file mode 100644
index 00000000..af29e4ad
--- /dev/null
+++ b/docs/dsl/fields.md
@@ -0,0 +1,78 @@
+# Fields
+
+```ruby
+# Use field for scalar values, arrays of scalar values, or even a Hash
+field :name
+field :tags
+
+# Add multiple fields at once
+fields :description, :price
+
+# Use object to render an object or Hash using another blueprint
+object :category, CategoryBlueprint
+
+# Use collection to render an array-like collection of objects
+collection :parts, PartBlueprint
+```
+
+## Options
+
+Fields accept a wide array of built-in options, and [extensions](./extensions.md) can define even more. [Find all built-in options here.](./options.md)
+
+```ruby
+field :description, default: "No description"
+collection :parts, PartBlueprint, exclude_if_empty: true
+```
+
+## Extracting field values
+
+Blueprinter is pretty smart about extracting field values from objects and Hashes, but there are ways to customize the behavior if needed.
+
+### Default behavior
+
+- For Hashes, Blueprinter will look for a key matching the field name - first with a Symbol, then a String.
+- For anything else, Blueprinter will look for a public method matching the field name.
+- The [from](./options.md#from) field option can be used to specify a different method or Hash key name.
+
+### Field blocks
+
+If you pass a block to your field, the default behavior will be bypassed and the block's return value will be used. It will be passed a [Field context](../api/context-objects.md#field-context) argument containing the object being rendered, among other things.
+
+```ruby
+field :description do |object, ctx|
+ object.description.upcase
+end
+
+# Blocks can call instance methods defined on your Blueprint
+collection :parts, PartBlueprint do |object, ctx|
+ active_parts object
+end
+
+def active_parts(object)
+ object.parts.select(&:active?)
+end
+```
+
+### Extracting with extensions
+
+The [around_field_value](../api/extensions.md#around_field_value), [around_object_value](../api/extensions.md#around_object_value), and [around_collection_value](../api/extensions.md#around_collection_value) middleware hooks can intercept extraction and return whatever values they want. [Learn more about using extensions](./extensions.md).
+
+```ruby
+class MyExtractor < Blueprinter::Extension
+ def around_field_value(ctx)
+ if ctx.field.options[:my_extractor] || ctx.blueprint.class.options[:my_extractor]
+ # If the field or blueprint has the "my_extractor" option, use custom extraction
+ my_custom_extraction(ctx.object, ctx.field)
+ else
+ # Otherwise use the default behavior
+ yield ctx
+ end
+ end
+
+ private
+
+ def my_custom_extraction(object, field)
+ # ...
+ end
+end
+```
diff --git a/docs/dsl/formatters.md b/docs/dsl/formatters.md
new file mode 100644
index 00000000..c97121e2
--- /dev/null
+++ b/docs/dsl/formatters.md
@@ -0,0 +1,18 @@
+# Formatters
+
+Declaratively format field values by class. You can define formatters anywhere in your blueprints: top level, [views](./views.md), and [partials](./partials.md).
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ # Strip whitespace from all strings
+ format(String) { |val| val.strip }
+
+ # Format all dates and times using ISO-8601
+ format Date, :iso8601
+ format Time, :iso8601
+
+ def iso8601(val)
+ val.iso8601
+ end
+end
+```
diff --git a/docs/dsl/index.md b/docs/dsl/index.md
new file mode 100644
index 00000000..c9fdde1d
--- /dev/null
+++ b/docs/dsl/index.md
@@ -0,0 +1,27 @@
+# Blueprinter DSL
+
+## Define your base class
+
+Define an `ApplicationBlueprint` for your blueprints to inherit from. Any global configuration goes here: common [fields](./fields.md), [views](./views.md), [partials](./partials.md), [formatters](./formatters.md), [extensions](./extensions.md), and [options](./options.md).
+
+```ruby
+class ApplicationBlueprint < Blueprinter::Blueprint
+ extensions << MyExtension.new
+ options[:exclude_if_nil] = true
+ field :id
+end
+```
+
+## Define blueprints for your models
+
+This blueprint inherits everything from `ApplicationBlueprint`, then adds a `name` field and two associations that will render using other blueprints.
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ object :category, CategoryBlueprint
+ collection :parts, PartBlueprint
+end
+```
+
+There's a lot more you can do with the Blueprinter DSL. [Fields](./fields.md) are a good place to start!
diff --git a/docs/dsl/options.md b/docs/dsl/options.md
new file mode 100644
index 00000000..8f5fa43e
--- /dev/null
+++ b/docs/dsl/options.md
@@ -0,0 +1,342 @@
+# Options
+
+Numerous options can be defined on Blueprints, views, partials, or individual fields. Some can also be passed to `render`.
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ # Blueprint options apply to all fields, associations, views, and partials in
+ # the Blueprint. They are inherited from the parent class but can be overridden.
+ options[:exclude_if_empty] = true
+
+ # Field options apply to individual fields or associations. They can override
+ # Blueprint options.
+ field :name, exclude_if_empty: false
+
+ # Options in views apply to all fields, associations, partials and nested views
+ # in the view. They inherit options from the Blueprint, or from parent views,
+ # and can override them.
+ view :foo do
+ options[:exclude_if_empty] = false
+ end
+
+ # Options in partials apply to all fields, associations, views, and partials in
+ # the partial. All of these are applied to the views that use the partial.
+ partial :bar do
+ options[:exclude_if_empty] = false
+ end
+
+ # Some options accept Procs/labmdas. These can call instance methods defined on
+ # your Blueprint. Or you can pass a method name as a symbol.
+ field :foo, if: ->(ctx) { long_complex_check? ctx }
+ field :bar, if: :long_complex_check?
+
+ def long_complex_check?(ctx)
+ # ...
+ end
+end
+
+# Passing a supported option to render will override what's in the blueprint
+WidgetBlueprint.render(widget, exclude_if_empty: false).to_json
+```
+
+For easier reference, options are grouped into the following categories:
+
+- [Default values](#default-values): Provide defaults for empty fields
+- [Conditional fields](#conditional-fields): Exclude fields based on conditions
+- [Field mapping](#field-mapping): Change how field values are extracted from objects
+- [Metadata](#other): Wrap or add metadata to the output
+
+#### A note about context objects
+
+Options that accept Procs, lambdas, or method names are usually passed a [Field context](../api/context-objects.md#field-context) argument. It contains the object being rendered as well as other useful information.
+
+## Default Values
+
+These options allow you to set default values for fields and associations, and customize when they're used.
+
+#### default
+
+A default value used when the field or assocation is nil.
+
+> *Available in field, object, collection*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+field :foo, default: "Foo"
+field :foo, default: ->(ctx) { "Foo" }
+field :foo, default: :foo
+
+def foo(ctx) = "Foo"
+```
+
+#### field_default
+
+Default value for any nil non-association field in its scope.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:field_default] = "Foo"
+options[:field_default] = ->(ctx) { "Foo" }
+options[:field_default] = :foo
+
+def foo(ctx) = "Foo"
+
+WidgetBluerpint.render(widget, field_default: "Foo").to_json
+```
+
+#### object_default
+
+Default value for any nil object field in its scope.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:object_default] = { name: "Foo" }
+options[:object_default] = ->(ctx) { { name: "Foo" } }
+options[:object_default] = :foo
+
+def foo(ctx) = { name: "Foo" }
+
+WidgetBluerpint.render(widget, object_default: { name: "Foo" }).to_json
+```
+
+#### collection_default
+
+Default value for any nil collection field.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:collection_default] = [{ name: "Foo" }]
+options[:collection_default] = ->(ctx) { [{ name: "Foo" }] }
+options[:collection_default] = :foo
+
+def foo(ctx) = [{ name: "Foo" }]
+
+WidgetBluerpint.render(widget, collection_default: [{ name: "Foo" }]).to_json
+```
+
+#### default_if
+
+Use the default value if the given Proc or method name returns truthy.
+
+> *Available in field, object, collection*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+field :foo, default: "Foo", default_if: ->(ctx) { ctx.object.disabled? }
+field :foo, default: "Foo", default_if: :disabled?
+
+def disabled?(ctx) = ctx.object.disabled?
+```
+
+#### field_default_if
+
+Same as [default_if](#default_if), but applies to any non-association field in scope.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:field_default_if] = ->(ctx) { ctx.object.disabled? }
+options[:field_default_if] = :disabled?
+
+def disabled?(ctx) = ctx.object.disabled?
+
+WidgetBluerpint.render(widget, field_default_if: :disabled?).to_json
+```
+
+#### object_default_if
+
+Same as [default_if](#default_if), but applies to any object field in scope.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:object_default_if] = ->(ctx) { ctx.object.disabled? }
+options[:object_default_if] = :disabled?
+
+def disabled?(ctx) = ctx.object.disabled?
+
+WidgetBluerpint.render(widget, object_default_if: :disabled?).to_json
+```
+
+#### collection_default_if
+
+Same as [default_if](#default_if), but applies to any collection field in scope.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:collection_default_if] = ->(ctx) { ctx.object.disabled? }
+options[:collection_default_if] = :disabled?
+
+def disabled?(ctx) = ctx.object.disabled?
+
+WidgetBluerpint.render(widget, collection_default_if: :disabled?).to_json
+```
+
+## Conditional Fields
+
+These options allow you to exclude fields from the output.
+
+#### exclude_if_nil
+
+Exclude fields if they're nil.
+
+> *Available in blueprint, view, partial, field, object, collection, render*
+
+```ruby
+options[:exclude_if_nil] = true
+
+field :description, exclude_if_nil: true
+
+WidgetBluerpint.render(widget, exclude_if_nil: true).to_json
+```
+
+#### exclude_if_empty
+
+Exclude fields if they're nil, or if they respond to `empty?` and it returns true.
+
+> *Available in blueprint, view, partial, field, object, collection, render*
+
+```ruby
+options[:exclude_if_empty] = true
+
+field :description, exclude_if_empty: true
+
+WidgetBluerpint.render(widget, exclude_if_empty: true).to_json
+```
+
+#### if
+
+Only include the field if the given Proc or method name returns truthy.
+
+> *Available in field, object, collection*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+field :foo, if: ->(ctx) { ctx.object.enabled? }
+field :foo, if: :enabled?
+
+def enabled?(ctx) = ctx.object.enabled?
+```
+
+#### field_if
+
+Only include non-association fields if the given Proc or method name returns truthy.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:field_if] = ->(ctx) { ctx.object.enabled? }
+options[:field_if] = :enabled?
+
+def enabled?(ctx) = ctx.object.enabled?
+
+WidgetBluerpint.render(widget, field_if: :enabled?).to_json
+```
+
+#### object_if
+
+Only include object fields if the given Proc or method name returns truthy.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:object_if] = ->(ctx) { ctx.object.enabled? }
+options[:object_if] = :enabled?
+
+def enabled?(ctx) = ctx.object.enabled?
+
+WidgetBluerpint.render(widget, object_if: :enabled?).to_json
+```
+
+#### collection_if
+
+Only include collection fields if the given Proc or method name returns truthy.
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Field context](../api/context-objects.md#field-context)
+
+```ruby
+options[:collection_if] = ->(ctx) { ctx.object.enabled? }
+options[:collection_if] = :enabled?
+
+def enabled?(ctx) = ctx.object.enabled?
+
+WidgetBluerpint.render(widget, collection_if: :enabled?).to_json
+```
+
+#### unless
+
+Inverse of [if](#if).
+
+#### field_unless
+
+Inverse of [field_if](#field_if).
+
+#### object_unless
+
+Inverse of [object_if](#object_if).
+
+#### collection_unless
+
+Inverse of [collection_if](#collection_if).
+
+## Field mapping
+
+These options let you change how fields values are extracted from your objects.
+
+#### from
+
+Populate the field using a method/Hash key other than the field name.
+
+> *Available in field, object, collection*
+
+```ruby
+field :desc, from: :description
+```
+
+## Metadata
+
+These options allow you to add metadata to the rendered output.
+
+#### root
+
+Pass a root key to wrap the output.
+
+> *Available in blueprint, view, partial, render*
+
+```ruby
+options[:root] = :data
+
+WidgetBlueprint.render(widget, root: :data).to_json
+```
+
+#### meta
+
+Add a `meta` key and data to the wrapped output (requires the `root` option).
+
+> *Available in blueprint, view, partial, render*\
+> **@param** [Result context](../api/context-objects.md#result-context)
+
+```ruby
+options[:root] = :data
+options[:meta] = { page: 1 }
+
+# If you pass a Proc/lambda, it can call instance methods defined on the Blueprint
+options[:meta] = ->(ctx) { { page: page_num(ctx) } }
+
+WidgetBlueprint
+ .render(widget, root: :data, meta: { page: params[:page] })
+ .to_json
+```
diff --git a/docs/dsl/partials.md b/docs/dsl/partials.md
new file mode 100644
index 00000000..82024faf
--- /dev/null
+++ b/docs/dsl/partials.md
@@ -0,0 +1,68 @@
+# Partials
+
+Partials allow you to compose views from reusable components. Just like views, partials can define [fields](./fields.md), [options](./options.md), [views](./views.md), other [partials](./partials.md), [formatters](./formatters.md), and [extensions](./extensions.md).
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+
+ view :foo do
+ use :associations
+ field :foo
+ end
+
+ view :bar do
+ use :associations, :description
+ field :bar
+ end
+
+ partial :associations do
+ object :category, CategoryBlueprint
+ collection :parts, PartBlueprint
+ end
+
+ partial :description do
+ field :description
+ end
+end
+```
+
+There are two ways of including partials: [appending with 'use'](#append-with-use) and [inserting with 'use!'](#inserting-with-use) (see [examples](#examples-of-use-and-use)).
+
+### Append with 'use'
+
+Partials are _appended_ to your view, giving them the opportunity to override your view's fields, options, etc. Precedence (highest to lowest) is:
+
+1. Definitions in the partial
+2. Definitions in the view
+3. Definitions inherited from the blueprint/parent views
+
+### Insert with 'use!'
+
+Partials are embedded immediately, _on that line_, allowing subsequent lines to override the partial. Precedence (highest to lowest) is:
+
+1. Definitions in the view _after_ `use!`
+2. Definitions in the partial
+3. Definitions in the view _before_ `use!`
+4. Definitions inherited from the blueprint/parent views
+
+### Examples of 'use' and 'use!'
+
+```ruby
+partial :no_empty_fields do
+ options[:field_if] = :og_field_logic
+ # other stuff
+end
+
+# :foo appends the partial, so it overrides the view's field_if
+view :foo do
+ use :no_empty_fields
+ options[:field_if] = :other_field_logic
+end
+
+# :bar inserts the partial, but the next line overrides the partial's field_if
+view :bar do
+ use! :no_empty_fields
+ options[:field_if] = :other_field_logic
+end
+```
diff --git a/docs/dsl/views.md b/docs/dsl/views.md
new file mode 100644
index 00000000..78397eff
--- /dev/null
+++ b/docs/dsl/views.md
@@ -0,0 +1,94 @@
+# Views
+
+Blueprints can define views to provide different representations of the data. A view inherits everything from its parent but is free to override as needed. In addition to [fields](./fields.md), views can define [options](./options.md), [partials](./partials.md), [formatters](./formatters.md), [extensions](./extensions.md), and [nested views](#nesting-views).
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ object :category, CategoryBlueprint
+
+ # The "with_parts" view inherits from "default" and adds a collection of parts
+ view :with_parts do
+ collection :parts, PartBlueprint
+ end
+
+ # Views can include other views
+ view :full do
+ use :with_parts
+ field :description
+ end
+end
+```
+
+At the top level of every Blueprint is an implicit view called `default`. The default view is used when no other is specified. All other views in the Blueprint inherit from it.
+
+### Nesting views
+
+You can nest views within views, allowing for a hierarchy of inheritance.
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ object :category, CategoryBlueprint
+
+ view :extended do
+ field :description
+ collection :parts, PartBlueprint
+
+ # The "extended.with_price" view adds a price field
+ view :with_price do
+ field :price
+ end
+ end
+```
+
+### Excluding fields
+
+Views can exclude select fields from parents, views they've included, or from [partials](./partials.md).
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ fields :name, :description, :price
+
+ view :minimal do
+ exclude :description, :price
+ end
+end
+```
+
+You can exclude and and all parent fields by creating an empty view:
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ fields :name, :description, :price
+
+ view :minimal, empty: true do
+ field :the_only_field
+ end
+end
+```
+
+### Referencing views
+
+When defining an association, you can choose a view from its blueprint:
+
+```ruby
+object :widget, WidgetBlueprint[:extended]
+```
+
+Nested views can be accessed with a dot syntax or a nested Hash syntax.
+
+```ruby
+collection :widgets, WidgetBlueprint["extended.with_price"]
+collection :widgets, WidgetBlueprint[:extended][:with_price]
+```
+
+### Inheriting from views
+
+You can inherit from another blueprint, or from one of its views:
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint[:with_timestamps]
+ # ...
+end
+```
diff --git a/docs/introduction.md b/docs/introduction.md
new file mode 100644
index 00000000..afbf11fd
--- /dev/null
+++ b/docs/introduction.md
@@ -0,0 +1,39 @@
+# Blueprinter
+
+### NOTE This is a WIP for API V2!
+
+Blueprinter is a JSON serializer for your business objects. It is designed to be simple, flexible, and performant.
+
+Upgrading from 1.x? [Read the upgrade guide!](./upgrading/index.md)
+
+### Installation
+
+```bash
+bundle add blueprinter
+```
+
+See [rubydoc.info/gems/blueprinter](https://www.rubydoc.info/gems/blueprinter) for generated API documentation.
+
+### Basic Usage
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ object :category, CategoryBlueprint
+ collection :parts, PartBlueprint
+
+ view :extended do
+ field :description
+ object :manufacturer, CompanyBlueprint
+ collection :vendors, CompanyBlueprint
+ end
+end
+
+# Render the default view to JSON
+WidgetBlueprint.render(widget).to_json
+
+# Render the extended view to a Hash
+WidgetBlueprint[:extended].render(widget).to_hash
+```
+
+Look interesting? [Learn the DSL!](./dsl/index.md)
diff --git a/docs/rendering.md b/docs/rendering.md
new file mode 100644
index 00000000..d60a5013
--- /dev/null
+++ b/docs/rendering.md
@@ -0,0 +1,61 @@
+# Rendering
+
+### Rendering to JSON
+
+```ruby
+WidgetBlueprint.render(widget).to_json
+```
+
+If you're using Rails, you may omit `.to_json` when calling `render json:`
+
+```ruby
+render json: WidgetBlueprint.render(widget)
+```
+
+Ruby's built-in `JSON` library is used by default. Alternatively, you can use the built-in [MultiJson extension](./dsl/extensions.md#multijson). Or for total control, implement the [around_result](./api/extensions.md#around_result) and call any serializer you like.
+
+### Rendering to a Hash
+
+```ruby
+WidgetBlueprint.render(widget).to_hash
+```
+
+### Rendering a view
+
+```ruby
+# Render a view
+WidgetBlueprint[:extended].render(widget).to_json
+
+# Render a nested view
+WidgetBlueprint["extended.price"].render(widget).to_json
+
+# These two both render the default view
+WidgetBlueprint.render(widget).to_json
+WidgetBlueprint[:default].render(widget).to_json
+```
+
+### Passing options
+
+An options hash can be passed to `render`. Read more about [options](./dsl/options.md).
+
+```ruby
+WidgetBlueprint.render(Widget.all, exclude_if_nil: true).to_json
+```
+
+### Rendering collections
+
+`render` will treat any `Enumerable`, except `Hash`, as an array of objects:
+
+```ruby
+WidgetBlueprint.render(Widget.all).to_json
+```
+
+If you wish to be explicit you may use `render_object` and `render_collection`:
+
+```ruby
+WidgetBlueprint.render_object(widget).to_json
+
+WidgetBlueprint.render_collection(Widget.all).to_json
+```
+
+Whatever you pass to `render_collection` must respond to `map`, yielding zero or more serializable objects, and returning an `Enumerable` with the mapped results.
diff --git a/docs/upgrading/configuration.md b/docs/upgrading/configuration.md
new file mode 100644
index 00000000..6d133a5d
--- /dev/null
+++ b/docs/upgrading/configuration.md
@@ -0,0 +1,27 @@
+# Configuration
+
+Blueprinter V2 has no concept of global configruation like V1's `Blueprinter.configure`. Instead, blueprints and views inherit configuration from their parent classes. By putting your "global" configuration into `ApplicationBlueprint`, all your application's blueprints and views will inherit it.
+
+```ruby
+class ApplicationBlueprint < Blueprinter::Blueprint
+ options[:exclude_if_nil] = true
+ extensions << MyExtension.new
+end
+```
+
+Read more about [options](../dsl/options.md) and [extensions](../dsl/extensions.md).
+
+## Overrides
+
+Child classes, [views](../dsl/views.md), and [partials](../dsl/partials.md) can override their inherited configuration.
+
+```ruby
+class MyBlueprint < ApplicationBlueprint
+ options[:exclude_if_nil] = false
+
+ view :foo do
+ options.clear
+ extensions.clear
+ end
+end
+```
diff --git a/docs/upgrading/customization.md b/docs/upgrading/customization.md
new file mode 100644
index 00000000..2b727d4e
--- /dev/null
+++ b/docs/upgrading/customization.md
@@ -0,0 +1,19 @@
+# Customization
+
+## Formatting
+
+Blueprinter V2 has a more generic approach to formatting, allowing any type of value to have formatting applied. [Learn more](../dsl/formatters.md).
+
+```ruby
+format(Date) { |date| date.iso8601 }
+```
+
+The [around_field_value](../api/extensions.md#around_field_value), [around_object_value](../api/extensions.md#around_object_value), and [around_collection_value](../api/extensions.md#around_collection_value) extension hooks can also be used.
+
+## Custom extractors
+
+Custom extraction in V2 can also be accomplished with the [around_field_value](../api/extensions.md#around_field_value), [around_object_value](../api/extensions.md#around_object_value), and [around_collection_value](../api/extensions.md#around_collection_value) hooks. [Read more](../dsl/fields.md#extracting-with-extensions).
+
+## Transformers
+
+Blueprinter V2's [extension hooks](../api/extensions.md) offer many ways to transform your inputs and outputs. The [around_blueprint](../api/extensions.md#around_blueprint) hook offers equivalent functionality to Legacy/V1 transformers.
diff --git a/docs/upgrading/extensions.md b/docs/upgrading/extensions.md
new file mode 100644
index 00000000..2c98285f
--- /dev/null
+++ b/docs/upgrading/extensions.md
@@ -0,0 +1,12 @@
+# Extensions
+
+The [V2 Extension API](../api/extensions.md), as well as the DSL for [enabling V2 extensions](../dsl/extensions.md), are vastly different and more powerful than V1. Legacy/V1 had only one extension hook: `pre_render`. V2 has [over a dozen](../api/extensions.md#hooks).
+
+## Porting pre_render
+
+Legacy/V1's `pre_render` hook does not exist in V2, but it has several possible replacements:
+
+* [around_result](../api/extensions.md#around_result) runs once around each entire result
+* [around_serialize_object](../api/extensions.md#around_serialize_object) runs around each object's serialization
+* [around_serialize_collection](../api/extensions.md#around_serialize_collection) runs around each collection's serialization
+* [around_blueprint](../api/extensions.md#around_blueprint) runs each time a blueprint serializes an object
diff --git a/docs/upgrading/fields.md b/docs/upgrading/fields.md
new file mode 100644
index 00000000..069db27e
--- /dev/null
+++ b/docs/upgrading/fields.md
@@ -0,0 +1,67 @@
+# Fields
+
+## Identifier field and view
+
+Blueprinter Legacy/V1 had a special feature for an `id` field and `identifier` view. Blueprinter V2 does not have this concept, but you can simulate it in your `ApplicationBlueprint`.
+
+```ruby
+class ApplicationBlueprint < Blueprinter::Blueprint
+ # Every Blueprint that inherits from ApplicationBlueprint will have this field
+ field :id
+
+ # Every Blueprint that inherits from ApplicationBlueprint will have this view,
+ # and it will only have the `id` field
+ view :identifier, empty: true do
+ field :id
+ end
+end
+```
+
+## Renaming fields
+
+In Blueprinter Legacy/V1, you could rename fields using the `name` option. Blueprinter V2 swaps the order and uses `from`. We believe this makes your blueprints more readable.
+
+In the following examples, both blueprints are populating the output field *description* from a source attribute named *desc*.
+
+```ruby
+# Legacy/V1
+field :desc, name: :description
+
+# V2
+field :description, from: :desc
+```
+
+## Associations
+
+Blueprinter Legacy/V1 figured out if associations were single items or arrays at runtime. Blueprinter V2 accounts for this in the DSL. Also, the `:blueprint` and `:view` options are gone.
+
+```ruby
+class WidgetBlueprint < ApplicationBlueprint
+ field :name
+ object :category, CategoryBlueprint
+ collection :parts, PartBlueprint
+
+ # specify a view
+ object :manufacturer, CompanyBlueprint[:extended]
+end
+```
+
+## Field order
+
+Blueprinter Legacy/V1 offered two options for ordering fields: `:name_asc` (default), and `:definition` (order they were defined in). Blueprinter V2 defaults to the order of definition. You can define a different order using the [around_blueprint_init](../api/extensions.md#around_blueprint_init) extension hook or the built-in [FieldOrder](../dsl/extensions.md#field-order) extension.
+
+The following replicates Legacy/V1's default field order using the built-in [FieldOrder](../dsl/extensions.md#field-order) extension.
+
+```ruby
+class ApplicationBlueprint < Blueprinter::Blueprint
+ extensions << Blueprinter::Extensions::FieldOrder.new do |a, b|
+ if a.name == :id
+ -1
+ elsif b.name == :id
+ 1
+ else
+ a.name <=> b.name
+ end
+ end
+end
+```
diff --git a/docs/upgrading/index.md b/docs/upgrading/index.md
new file mode 100644
index 00000000..676b65c4
--- /dev/null
+++ b/docs/upgrading/index.md
@@ -0,0 +1,39 @@
+# Upgrading to API V2
+
+You have two options when updating from the legacy/V1 API: [full update](#full-update) or [incremental update](#incremental-update).
+
+Regardless which you choose, you'll need to familiarize yourself with the [new DSL](../dsl/index.md) and [API](../api/index.md). The rest of this section will focus on the differences between V1 and V2.
+
+## Full update
+
+Update `blueprinter` to 2.x. All of your blueprints will need updated to use the [new DSL](../dsl/index.md). If you're making use of extensions, custom extractors, or transformers, they'll also need updated to the [new API](../api/index.md).
+
+## Incremental update
+
+Larger applications may find it easier to update incrementally. Update `blueprinter` to 1.2.x, which contains both the legacy/V1 and V2 APIs. They can be used side-by-side.
+
+```ruby
+# A legacy/V1 blueprint
+class WidgetBlueprint < Blueprinter::Blueprint
+ field :name
+
+ view :with_desc do
+ field :description
+ end
+
+ view :with_category do
+ # Using a V2 blueprint in a legacy/V1 blueprint
+ association :category, blueprint: CategoryBlueprint, view: :extended
+ end
+end
+
+# A V2 blueprint
+class CategoryBlueprint < ApplicationBlueprint
+ field :name
+
+ view :extended do
+ # Using a legacy/V1 blueprint in a V2 blueprint
+ collection :widgets, WidgetBlueprint[:with_desc]
+ end
+end
+```
diff --git a/docs/upgrading/reflection.md b/docs/upgrading/reflection.md
new file mode 100644
index 00000000..18800070
--- /dev/null
+++ b/docs/upgrading/reflection.md
@@ -0,0 +1,66 @@
+# Reflection
+
+The [V2 Reflection API](../api/reflection.md) has very few changes from Legacy/V1.
+
+## Reflecting on fields
+
+Regular fields (no change):
+
+```ruby
+MyBlueprint.reflections[:default].fields
+```
+
+Objects and collections:
+
+```ruby
+# Legacy/V1 does not differentiate between objects and collections
+MyV1Blueprint.reflections[:default].associations
+
+# V2 does
+MyV2Blueprint.reflections[:default].objects
+MyV2Blueprint.reflections[:default].collections
+```
+
+## Field names
+
+[V2's field metadata](../api/reflection.md#field-metadata) is similar, but there's an important different in `name`.
+
+#### Legacy/V1
+
+In Legacy/V1, `name` refers to what the field is called in the *input*.
+
+```ruby
+class MyV1Blueprint < Blueprinter::Base
+ field :foo, name: :bar
+end
+
+ref = MyV1Blueprint.reflections[:default]
+
+# What the field is called in the source object
+ref.fields[:foo].name
+=> :foo
+
+# What the field will be called in the output
+ref.fields[:foo].display_name
+=> :bar
+```
+
+#### V2
+
+In V2, `name` refers to what the field is called in the *output*. Note that this change is also reflected in the Hash key.
+
+```ruby
+class MyV2Blueprint < Blueprinter::Blueprint
+ field :bar, from: :foo
+end
+
+ref = MyV1Blueprint.reflections[:default]
+
+# What the field will be called in the output
+ref.fields[:bar].name
+=> :bar
+
+# What the field is called in the source object
+ref.fields[:bar].from
+=> :foo
+```
diff --git a/docs/upgrading/rendering.md b/docs/upgrading/rendering.md
new file mode 100644
index 00000000..420aea47
--- /dev/null
+++ b/docs/upgrading/rendering.md
@@ -0,0 +1,37 @@
+# Rendering
+
+You can read the full [rendering documentation here](../rendering.md). This page highlights the main differences between V1 and V2.
+
+### Rendering to JSON
+
+If you're using Rails's `render json:`, V2 blueprints should continue to work like Legacy/V1:
+
+```ruby
+render json: WidgetBlueprint.render(widget)
+```
+
+Otherwise, it now looks like this:
+
+```ruby
+WidgetBlueprint.render(widget).to_json
+```
+
+### Rendering to Hash
+
+```ruby
+WidgetBlueprint.render(widget).to_hash
+```
+
+### Views
+
+V2's preferred method of rendering views is:
+
+```ruby
+WidgetBlueprint[:extended].render(widget).to_json
+```
+
+However, the [ViewOption](../dsl/extensions.md#viewoption) extension can be enabled to allow V1-style view rendering:
+
+```ruby
+WidgetBlueprint.render(widget, view: :extended).to_json
+```
diff --git a/docs/v1.md b/docs/v1.md
new file mode 100644
index 00000000..7c5b7cb1
--- /dev/null
+++ b/docs/v1.md
@@ -0,0 +1,3 @@
+# Legacy/V1 Docs
+
+TODO copy from old README
diff --git a/spec/v2/partials_spec.rb b/spec/v2/partials_spec.rb
index 10af3405..7b509b4a 100644
--- a/spec/v2/partials_spec.rb
+++ b/spec/v2/partials_spec.rb
@@ -105,6 +105,10 @@
blueprint = Class.new(Blueprinter::V2::Base) do
view :foo do
field :name
+
+ view :zorp do
+ field :zorp
+ end
end
view :bar do
@@ -113,8 +117,8 @@
end
end
- refs = blueprint.reflections
- expect(refs[:bar].fields.keys).to eq %i(name description).sort
+ expect(blueprint.reflections[:bar].fields.keys.sort).to eq %i(name description).sort
+ expect(blueprint.reflections[:"bar.zorp"].fields.keys.sort).to eq %i(name zorp description).sort
end
context 'precedence' do
diff --git a/theme/custom.css b/theme/custom.css
new file mode 100644
index 00000000..5505d4fb
--- /dev/null
+++ b/theme/custom.css
@@ -0,0 +1,5 @@
+.hooks {
+ font-family: var(--mono-font) !important;
+ font-size: var(--code-font-size);
+ white-space: pre-wrap;
+}
diff --git a/theme/favicon.svg b/theme/favicon.svg
new file mode 100644
index 00000000..c5b65fd7
--- /dev/null
+++ b/theme/favicon.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/theme/highlight.js b/theme/highlight.js
new file mode 100644
index 00000000..aa42ae09
--- /dev/null
+++ b/theme/highlight.js
@@ -0,0 +1,382 @@
+/*!
+ Highlight.js v11.10.0 (git: 366a8bd012)
+ (c) 2006-2024 Josh Goebel and other contributors
+ License: BSD-3-Clause
+ */
+var hljs=function(){"use strict";function e(t){
+return t instanceof Map?t.clear=t.delete=t.set=()=>{
+throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
+throw Error("set is read-only")
+}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
+const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i)
+})),t}class t{constructor(e){
+void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
+ignoreMatch(){this.isMatchIgnored=!0}}function n(e){
+return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
+}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
+;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope
+;class o{constructor(e,t){
+this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
+this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{
+if(e.startsWith("language:"))return e.replace("language:","language-")
+;if(e.includes(".")){const n=e.split(".")
+;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
+}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)}
+closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){
+this.buffer+=``}}const r=(e={})=>{const t={children:[]}
+;return Object.assign(t,e),t};class a{constructor(){
+this.rootNode=r(),this.stack=[this.rootNode]}get top(){
+return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
+this.top.children.push(e)}openNode(e){const t=r({scope:e})
+;this.add(t),this.stack.push(t)}closeNode(){
+if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
+for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
+walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
+return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
+t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
+"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
+a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e}
+addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
+this.closeNode()}__addSublanguage(e,t){const n=e.root
+;t&&(n.scope="language:"+t),this.add(n)}toHTML(){
+return new o(this,this.options).value()}finalize(){
+return this.closeAllNodes(),!0}}function l(e){
+return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")}
+function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")}
+function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{
+const t=e[e.length-1]
+;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
+})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"}
+function p(e){return RegExp(e.toString()+"|").exec("").length-1}
+const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
+;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
+;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break}
+s+=i.substring(0,e.index),
+i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],
+"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)}
+const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={
+begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'",
+illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
+contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t,
+contains:[]},n);s.contains.push({scope:"doctag",
+begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
+end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
+;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
+;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s
+},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({
+__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
+scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N,
+C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number",
+begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{
+"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
+t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E,
+MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0},
+NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w,
+PHRASAL_WORDS_MODE:{
+begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
+},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
+end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
+RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
+SHEBANG:(e={})=>{const t=/^#![ ]*\//
+;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t,
+end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
+TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
+UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){
+"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){
+void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){
+t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
+e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
+void 0===e.relevance&&(e.relevance=0))}function L(e,t){
+Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){
+if(e.match){
+if(e.begin||e.end)throw Error("begin & end are not supported with match")
+;e.begin=e.match,delete e.match}}function P(e,t){
+void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return
+;if(e.starts)throw Error("beforeMatch cannot be used with starts")
+;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
+})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
+relevance:0,contains:[Object.assign(n,{endsParent:!0})]
+},e.relevance=0,delete n.beforeMatch
+},H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword"
+;function $(e,t,n=C){const i=Object.create(null)
+;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
+Object.assign(i,$(e[n],t,n))})),i;function s(e,n){
+t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
+;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){
+return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{
+console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{
+z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
+},K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={}
+;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1])
+;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{
+e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
+delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
+_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
+}),(e=>{if(Array.isArray(e.begin)){
+if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
+K
+;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"),
+K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{
+if(Array.isArray(e.end)){
+if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
+K
+;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"),
+K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){
+function t(t,n){
+return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
+}class n{constructor(){
+this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
+addRule(e,t){
+t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
+this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
+;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|"
+}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
+;const t=this.matcherRe.exec(e);if(!t)return null
+;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
+;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
+this.rules=[],this.multiRegexes=[],
+this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
+if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
+;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
+t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
+return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
+this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
+const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
+;let n=t.exec(e)
+;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
+const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
+return n&&(this.regexIndex+=n.position+1,
+this.regexIndex===this.count&&this.considerAll()),n}}
+if(e.compilerExtensions||(e.compilerExtensions=[]),
+e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
+;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o
+;if(o.isCompiled)return a
+;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))),
+o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null
+;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords),
+c=o.keywords.$pattern,
+delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)),
+a.keywordPatternRe=t(c,!0),
+r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),
+o.end&&(a.endRe=t(a.end)),
+a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)),
+o.illegal&&(a.illegalRe=t(o.illegal)),
+o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
+variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{
+starts:e.starts?i(e.starts):null
+}):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a)
+})),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s
+;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
+}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
+}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){
+return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{
+constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
+const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{
+const i=Object.create(null),s=Object.create(null),o=[];let r=!0
+;const a="Could not find the language '{}', did you forget to load/include a language module?",l={
+disableAutodetect:!0,name:"Plain text",contains:[]};let p={
+ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
+languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
+cssSelector:"pre code",languages:null,__emitter:c};function b(e){
+return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s=""
+;"object"==typeof t?(i=e,
+n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."),
+G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
+s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o)
+;const r=o.result?o.result:E(o.language,o.code,n)
+;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){
+const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R)
+;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=""
+;for(;t;){n+=R.substring(e,t.index)
+;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){
+const[e,i]=o
+;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{
+const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0]
+;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i
+;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{
+if(""===R)return;let e=null;if("string"==typeof N.subLanguage){
+if(!i[N.subLanguage])return void M.addText(R)
+;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
+}else e=x(R,N.subLanguage.length?N.subLanguage:null)
+;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language)
+})():l(),R=""}function u(e,t){
+""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1
+;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}
+const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}}
+function h(e,t){
+return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope),
+e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
+R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{
+value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t)
+;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e)
+;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){
+for(;e.endsParent&&e.parent;)e=e.parent;return e}}
+if(e.endsWithParent)return f(e.parent,n,i)}function b(e){
+return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){
+const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N
+;N.endScope&&N.endScope._wrap?(g(),
+u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
+d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t),
+g(),o.excludeEnd&&(R=t));do{
+N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent
+}while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length}
+let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0
+;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){
+if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`)
+;throw t.languageName=e,t.badRule=w.rule,t}return 1}
+if(w=o,"begin"===o.type)return(e=>{
+const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]]
+;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
+;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
+g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o)
+;if("illegal"===o.type&&!s){
+const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"')
+;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e}
+if("illegal"===o.type&&""===a)return 1
+;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches")
+;return R+=a,a.length}const _=O(e)
+;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
+;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[]
+;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope)
+;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{
+if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){
+I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A
+;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e)
+;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e,
+value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){
+if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),
+illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,
+context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{
+language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
+;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{
+const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
+;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1)))
+;s.unshift(n);const o=s.sort(((e,t)=>{
+if(e.relevance!==t.relevance)return t.relevance-e.relevance
+;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1
+;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r
+;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{
+let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
+;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1])
+;return t||(X(a.replace("{}",n[1])),
+X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
+return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return
+;if(N("before:highlightElement",{el:e,language:n
+}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
+;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
+console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
+console.warn("The element with unescaped HTML:"),
+console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML)
+;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
+;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n
+;e.classList.add("hljs"),e.classList.add("language-"+i)
+})(e,n,o.language),e.result={language:o.language,re:o.relevance,
+relevance:o.relevance},o.secondBest&&(e.secondBest={
+language:o.secondBest.language,relevance:o.secondBest.relevance
+}),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){
+"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0
+}function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
+function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
+s[e.toLowerCase()]=t}))}function k(e){const t=O(e)
+;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{
+e[n]&&e[n](t)}))}
+"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
+y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_,
+highlightElement:w,
+highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"),
+G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)},
+initHighlighting:()=>{
+_(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
+initHighlightingOnLoad:()=>{
+_(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
+},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){
+if(W("Language definition for '{}' could not be registered.".replace("{}",e)),
+!r)throw t;W(t),s=l}
+s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{
+languageName:e})},unregisterLanguage:e=>{delete i[e]
+;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
+listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v,
+autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{
+e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
+e["before:highlightBlock"](Object.assign({block:t.el},t))
+}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
+e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)},
+removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{
+r=!1},n.safeMode=()=>{r=!0},n.versionString="11.10.0",n.regex={concat:h,
+lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
+;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n
+},ne=te({});return ne.newInstance=()=>te({}),ne}()
+;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `bash` grammar compiled for Highlight.js 11.10.0 */
+(()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/,
+end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{
+className:"variable",variants:[{
+begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={
+className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]
+},i=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),c={
+begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,
+end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/,
+contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(o);const r={begin:/\$?\(\(/,
+end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]
+},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
+}),m={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
+contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
+name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,
+keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"],
+literal:["true","false"],
+built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]
+},contains:[l,e.SHEBANG(),m,r,i,c,{match:/(\/[a-z._-]+)+/},o,{match:/\\"/},{
+className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}})()
+;hljs.registerLanguage("bash",e)})();/*! `ruby` grammar compiled for Highlight.js 11.10.0 */
+(()=>{var e=(()=>{"use strict";return e=>{
+const n=e.regex,a="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",s=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(s,/(::\w+)*/),t={
+"variable.constant":["__FILE__","__LINE__","__ENCODING__"],
+"variable.language":["self","super"],
+keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"],
+built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"],
+literal:["true","false","nil"]},c={className:"doctag",begin:"@[A-Za-z]+"},r={
+begin:"#<",end:">"},b=[e.COMMENT("#","$",{contains:[c]
+}),e.COMMENT("^=begin","^=end",{contains:[c],relevance:10
+}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],l={className:"subst",begin:/#\{/,
+end:/\}/,keywords:t},d={className:"string",contains:[e.BACKSLASH_ESCAPE,l],
+variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{
+begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{
+begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?,end:/>/},{begin:/%[qQwWx]?\//,
+end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{
+begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{
+begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{
+begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{
+begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{
+begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)),
+contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,
+contains:[e.BACKSLASH_ESCAPE,l]})]}]},o="[0-9](_?[0-9])*",g={className:"number",
+relevance:0,variants:[{
+begin:`\\b([1-9](_?[0-9])*|0)(\\.(${o}))?([eE][+-]?(${o})|r)?i?\\b`},{
+begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"
+},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{
+begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{
+begin:"\\b0(_?[0-7])+r?i?\\b"}]},_={variants:[{match:/\(\)/},{
+className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0,
+keywords:t}]},u=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{
+match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class",
+4:"title.class.inherited"},keywords:t},{match:[/(include|extend)\s+/,i],scope:{
+2:"title.class"},keywords:t},{relevance:0,match:[i,/\.new[. (]/],scope:{
+1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
+className:"variable.constant"},{relevance:0,match:s,scope:"title.class"},{
+match:[/def/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[_]},{
+begin:e.IDENT_RE+"::"},{className:"symbol",
+begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",
+begin:":(?!\\s)",contains:[d,{begin:a}],relevance:0},g,{className:"variable",
+begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{
+className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0,
+relevance:0,keywords:t},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",
+keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,l],
+illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{
+begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",
+end:"\\][a-z]*"}]}].concat(r,b),relevance:0}].concat(r,b)
+;l.contains=u,_.contains=u;const m=[{begin:/^\s*=>/,starts:{end:"$",contains:u}
+},{className:"meta.prompt",
+begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",
+starts:{end:"$",keywords:t,contains:u}}];return b.unshift(r),{name:"Ruby",
+aliases:["rb","gemspec","podspec","thor","irb"],keywords:t,illegal:/\/\*/,
+contains:[e.SHEBANG({binary:"ruby"})].concat(m).concat(b).concat(u)}}})()
+;hljs.registerLanguage("ruby",e)})();
\ No newline at end of file
diff --git a/theme/hljs-overrides.css b/theme/hljs-overrides.css
new file mode 100644
index 00000000..3bcfc9c7
--- /dev/null
+++ b/theme/hljs-overrides.css
@@ -0,0 +1,60 @@
+/* Override the default syntax highlighting to sorta match GitHub's */
+
+code.language-ruby.hljs .hljs-params {
+ color: inherit;
+}
+
+/* Assume dark theme by default */
+
+code.language-ruby.hljs .hljs-keyword {
+ color: #f46767;
+}
+
+code.language-ruby.hljs .hljs-symbol,
+code.language-ruby.hljs .hljs-string {
+ color: #96d0ff;
+}
+
+code.language-ruby.hljs .hljs-title.function_ {
+ color: #dcbdfb;
+}
+
+code.language-ruby.hljs .hljs-title.class_ {
+ color: #f69d50;
+}
+
+code.language-ruby.hljs .hljs-variable,
+code.language-ruby.hljs .hljs-literal {
+ color: #6cb6ff;
+}
+
+/* Variants for light themes */
+
+html.light code.language-ruby.hljs .hljs-keyword,
+html.rust code.language-ruby.hljs .hljs-keyword {
+ color: #cf222e;
+}
+
+html.light code.language-ruby.hljs .hljs-title.function_,
+html.rust code.language-ruby.hljs .hljs-title.function_ {
+ color: #6639ba;
+}
+
+html.light code.language-ruby.hljs .hljs-title.class_,
+html.rust code.language-ruby.hljs .hljs-title.class_ {
+ color: #953800;
+}
+
+html.light code.language-ruby.hljs .hljs-variable,
+html.light code.language-ruby.hljs .hljs-literal,
+html.rust code.language-ruby.hljs .hljs-variable,
+html.rust code.language-ruby.hljs .hljs-literal {
+ color: #0550ae;
+}
+
+html.light code.language-ruby.hljs .hljs-symbol,
+html.light code.language-ruby.hljs .hljs-string,
+html.rust code.language-ruby.hljs .hljs-symbol,
+html.rust code.language-ruby.hljs .hljs-string {
+ color: #1554b3;
+}