rest_framework 0.8.16 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ ---
2
+ layout: default
3
+ title: Routers
4
+ position: 1
5
+ slug: routers
6
+ ---
7
+ # Routers
8
+
9
+ You can route RESTful controllers with the normal utilities that Rails provides. However, the REST
10
+ framework also provides some helpers to route controllers using attributes of the controllers
11
+ themselves.
12
+
13
+ ## Routing the API Root
14
+
15
+ Your API root should probably have some content describing how one can authenticate with the API,
16
+ and what sub-controllers exist in the API. The API root can also be a great place to put singleton
17
+ actions on your API, if needed.
18
+
19
+ There are two common ways for an API root to be implemented.
20
+
21
+ ### Inherited API Root
22
+
23
+ You likely have an API controller that your other controllers inherit from, like this:
24
+
25
+ ```shell
26
+ app/controllers/
27
+ ├── api
28
+ │   ├── groups_controller.rb
29
+ │   ├── movies_controller.rb
30
+ │ ├── marbles_controller.rb
31
+ │ └── users_controller.rb
32
+ ├── api_controller.rb
33
+ └── application_controller.rb
34
+ ```
35
+
36
+ If your controllers in the `api` directory inherit from `ApiController` and you want root actions on
37
+ `ApiController`, then you would setup your root route in the top-level namespace, like this:
38
+
39
+ ```ruby
40
+ Rails.application.routes.draw do
41
+ rest_root :api
42
+ namespace :api do
43
+ ...
44
+ end
45
+ end
46
+ ```
47
+
48
+ However, note that actions defined on `ApiController` are defined on all sub-controllers, so if
49
+ you're using `match` rules to route controllers, then this may lead to undesired behavior.
50
+
51
+ ### Dedicated API Root
52
+
53
+ A better way might be to dedicate a controller for the API root, which would prevent actions and
54
+ properties defined on the root API from propagating to the rest of the namespace through
55
+ inheritance. You would add a `RootController` so your directory would look like this:
56
+
57
+ ```shell
58
+ app/controllers/
59
+ ├── api
60
+ │   ├── groups_controller.rb
61
+ │   ├── movies_controller.rb
62
+ │   ├── root_controller.rb
63
+ │ ├── marbles_controller.rb
64
+ │ └── users_controller.rb
65
+ ├── api_controller.rb
66
+ └── application_controller.rb
67
+ ```
68
+
69
+ Now you can route the root in the `:api` namespace, like this:
70
+
71
+ ```ruby
72
+ Rails.application.routes.draw do
73
+ namespace :api do
74
+ rest_root # By default this will find and route `Api::RootController`.
75
+ ...
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## Resourceful Routing
81
+
82
+ The REST Framework provides resourceful routers `rest_resource` and `rest_resources`, analogous to
83
+ Rails' `resource` and `resources`. These routers will inspect their corresponding controllers and
84
+ route `extra_actions` (aliased with `extra_collection_actions`) and `extra_member_actions`
85
+ automatically.
86
+
87
+ ```ruby
88
+ Rails.application.routes.draw do
89
+ namespace :api do
90
+ rest_root
91
+ rest_resource :user
92
+ rest_resources :movies
93
+ end
94
+ end
95
+ ```
96
+
97
+ ## Non-resourceful Routing
98
+
99
+ The `rest_route` non-resourceful router does not route the standard resource routes (`index`,
100
+ `create`, `show`, `list`, `update`, `delete`). Any actions must be defined as `extra_actions` on the
101
+ controller.
102
+
103
+ ```ruby
104
+ Rails.application.routes.draw do
105
+ namespace :api do
106
+ rest_root
107
+ rest_route :network
108
+ end
109
+ end
110
+ ```
@@ -0,0 +1,293 @@
1
+ ---
2
+ layout: default
3
+ title: Controller Mixins
4
+ position: 2
5
+ slug: controller-mixins
6
+ ---
7
+ # Controller Mixins
8
+
9
+ This is the core of the REST Framework. Generally speaking, projects already have an existing
10
+ controller inheritance hierarchy, so we want developers to be able to maintain that project
11
+ structure while leveraging the power of the REST Framework. Also, different controllers which
12
+ inherit from the same parent often need different REST Framework mixins. For these reasons, REST
13
+ Framework provides the controller functionality as modules that you mix into your controllers.
14
+
15
+ ## Response Rendering
16
+
17
+ Before we go into the various controller mixins, one of the core capabilities of the REST Framework
18
+ is to provide system-consumable responses along side a browsable API for developers. While you can
19
+ use Rails' builtin rendering tools, such as `render`, the REST Framework provides a rendering helper
20
+ called `api_response`. This helper allows you to return a browsable API response for the `html`
21
+ format which shows you what the JSON/XML response would look like, along with faster and lighter
22
+ responses for `json` and `xml` formats.
23
+
24
+ Here is an example:
25
+
26
+ ```ruby
27
+ class ApiController < ApplicationController
28
+ include RESTFramework::BaseControllerMixin
29
+ self.extra_actions = {test: :get}
30
+
31
+ def test
32
+ api_response({message: "Test successful!"})
33
+ end
34
+ end
35
+ ```
36
+
37
+ ## `BaseControllerMixin`
38
+
39
+ To transform a controller into the simplest possible RESTful controller, you can include
40
+ `BaseControllerMixin`, which provides a simple `root` action so it can be used at the API root.
41
+
42
+ ```ruby
43
+ class ApiController < ApplicationController
44
+ include RESTFramework::BaseControllerMixin
45
+ end
46
+ ```
47
+
48
+ ### Controller Attributes
49
+
50
+ You can customize the behavior of `BaseControllerMixin` by setting or mutating various class
51
+ attributes.
52
+
53
+ #### `singleton_controller`
54
+
55
+ This property primarily controls the routes that are generated for a RESTful controller. If you use
56
+ `api_resource`/`api_resources` to define whether the generates routes are for a collection or for
57
+ a single member, then you do not need to use this property. However, if you are autogenerating those
58
+ routers, then `singleton_controller` will tell REST Framework whether to provide collection routes
59
+ (when `singleton_controller` is falsy) or member routes (when `singleton_controller` is truthy). To
60
+ read more about singular vs plural routing, see Rails' documentation here:
61
+ https://guides.rubyonrails.org/routing.html#singular-resources.
62
+
63
+ #### `extra_actions`
64
+
65
+ This property defines extra actions on the controller to be routed. It is a hash of
66
+ `endpoint -> method(s)` (where `method(s)` can be a method symbol or an array of method symbols).
67
+
68
+ ```ruby
69
+ class ApiController < ApplicationController
70
+ include RESTFramework::BaseControllerMixin
71
+ self.extra_actions = {test: :get}
72
+
73
+ def test
74
+ api_response({message: "Test successful!"})
75
+ end
76
+ end
77
+ ```
78
+
79
+ Or with multiple methods:
80
+
81
+ ```ruby
82
+ class ApiController < ApplicationController
83
+ include RESTFramework::BaseControllerMixin
84
+ self.extra_actions = {test: [:get, :post]}
85
+
86
+ def test
87
+ api_response({message: "Test successful!"})
88
+ end
89
+ end
90
+ ```
91
+
92
+ If your action conflicts with a builtin method, then you can also override the path:
93
+
94
+ ```ruby
95
+ class ApiController < ApplicationController
96
+ include RESTFramework::BaseControllerMixin
97
+
98
+ # This will route `test_action` to `/test`, in case there is already a `test` method that cannot
99
+ # be overridden.
100
+ self.extra_actions = {test_action: {path: :test, methods: :get}}
101
+
102
+ def test_action
103
+ api_response({message: "Test successful!"})
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## `ModelControllerMixin`
109
+
110
+ `ModelControllerMixin` assists with providing the standard model CRUD (create, read, update,
111
+ destroy) for your controller. This is the most commonly used mixin since it provides default
112
+ behavior for models which matches Rails' default routing.
113
+
114
+ ```ruby
115
+ class Api::MoviesController < ApiController
116
+ include RESTFramework::ModelControllerMixin
117
+ end
118
+ ```
119
+
120
+ By default, all columns and associations are included in `self.fields`, which can be helpful when
121
+ developing an administrative API. For user-facing APIs, however, `self.fields` should always be
122
+ explicitly defined.
123
+
124
+ ### Controller Attributes
125
+
126
+ You can customize the behavior of `ModelControllerMixin` by setting or mutating various class
127
+ attributes.
128
+
129
+ #### `model`
130
+
131
+ The `model` property allows you to define the model if it is not obvious from the controller name.
132
+
133
+ ```ruby
134
+ class Api::CoolMoviesController < ApiController
135
+ include RESTFramework::ModelControllerMixin
136
+
137
+ self.model = Movie
138
+ end
139
+ ```
140
+
141
+ #### `recordset`
142
+
143
+ The `recordset` property allows you to define the set of records this API should be limited to. If
144
+ you need to change the recordset based on properties of the request, then you can override the
145
+ `get_recordset()` method.
146
+
147
+ ```ruby
148
+ class Api::CoolMoviesController < ApiController
149
+ include RESTFramework::ModelControllerMixin
150
+
151
+ self.recordset = Movie.where(cool: true).order({id: :asc})
152
+ end
153
+ ```
154
+
155
+ #### `extra_member_actions`
156
+
157
+ The `extra_member_actions` property allows you to define additional actions on individual records.
158
+
159
+ ```ruby
160
+ class Api::MoviesController < ApiController
161
+ include RESTFramework::ModelControllerMixin
162
+
163
+ self.extra_member_actions = {disable: :post}
164
+
165
+ def disable
166
+ @record = self.get_record # REST Framework will rescue ActiveRecord::RecordNotFound
167
+
168
+ # REST Framework will rescue ActiveRecord::RecordInvalid or ActiveRecord::RecordNotSaved
169
+ @record.update!(enabled: false)
170
+
171
+ return api_response(@record)
172
+ end
173
+ end
174
+ ```
175
+
176
+ #### `fields`
177
+
178
+ The `fields` property defines the default fields for serialization and for parameters allowed from
179
+ the body or query string.
180
+
181
+ ```ruby
182
+ class Api::MoviesController < ApiController
183
+ include RESTFramework::ModelControllerMixin
184
+
185
+ self.fields = [:id, :name]
186
+ end
187
+ ```
188
+
189
+ #### `action_fields`
190
+
191
+ The `action_fields` property is similar to `fields`, but allows you to define different fields for
192
+ different actions. A good example is to serialize expensive computed properties only in the `show`
193
+ action, but not in the `list` action (where many records are serialized).
194
+
195
+ ```ruby
196
+ class Api::MoviesController < ApiController
197
+ include RESTFramework::ModelControllerMixin
198
+
199
+ self.fields = [:id, :name]
200
+ self.action_fields = {
201
+ show: [:id, :name, :some_expensive_computed_property],
202
+ }
203
+ end
204
+ ```
205
+
206
+ #### `native_serializer_config`
207
+
208
+ These properties define the serializer configuration if you are using the native `ActiveModel`
209
+ serializer. You can also specify serializers for singular/plural
210
+
211
+ ```ruby
212
+ class Api::MoviesController < ApiController
213
+ include RESTFramework::ModelControllerMixin
214
+
215
+ self.native_serializer_config = {
216
+ only: [:id, :name],
217
+ methods: [:active, :some_expensive_computed_property],
218
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
219
+ }
220
+
221
+ # Or you could configure a default and a plural serializer:
222
+ self.native_serializer_plural_config = {
223
+ only: [:id, :name],
224
+ methods: [:active],
225
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
226
+ }
227
+ self.native_serializer_config = {
228
+ only: [:id, :name],
229
+ methods: [:active, :some_expensive_computed_property],
230
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
231
+ }
232
+
233
+ # Or you could configure a default and a singular serializer:
234
+ self.native_serializer_config = {
235
+ only: [:id, :name],
236
+ methods: [:active],
237
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
238
+ }
239
+ self.native_serializer_singular_config = {
240
+ only: [:id, :name],
241
+ methods: [:active, :some_expensive_computed_property],
242
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
243
+ }
244
+ end
245
+ ```
246
+
247
+ #### `allowed_parameters` / `allowed_action_parameters`
248
+
249
+ These properties define the permitted parameters to be used in the request body for create/update
250
+ actions. If you need different allowed parameters, then you can also override the
251
+ `get_create_params` or `get_update_params` methods.
252
+
253
+ ```ruby
254
+ class Api::MoviesController < ApiController
255
+ include RESTFramework::ModelControllerMixin
256
+
257
+ self.allowed_parameters = [:name]
258
+ end
259
+ ```
260
+
261
+ #### `create_from_recordset` (default: `true`)
262
+
263
+ The `create_from_recordset` attribute (`true` by default) is a boolean to control the behavior in
264
+ the `create` action. If it is disabled, records will not be created from the filtered recordset, but
265
+ rather will be created directly from the model interface.
266
+
267
+ For example, if this is your controller:
268
+
269
+ ```ruby
270
+ class Api::CoolMoviesController < ApiController
271
+ include RESTFramework::ModelControllerMixin
272
+
273
+ def get_recordset
274
+ return Movie.where(cool: true)
275
+ end
276
+ end
277
+ ```
278
+
279
+ Then if you hit the `create` action with the payload `{name: "Superman"}`, it will also set `cool`
280
+ to `true` on the new record, because that property is inherited from the recordset.
281
+
282
+ ## `ReadOnlyModelControllerMixin`
283
+
284
+ `ReadOnlyModelControllerMixin` only enables list/show actions. In this example, since we're naming
285
+ this controller in a way that doesn't make the model obvious, we can set that explicitly:
286
+
287
+ ```ruby
288
+ class Api::ReadOnlyMoviesController < ApiController
289
+ include RESTFramework::ReadOnlyModelControllerMixin
290
+
291
+ self.model = Movie
292
+ end
293
+ ```
@@ -0,0 +1,60 @@
1
+ ---
2
+ layout: default
3
+ title: Serializers
4
+ position: 3
5
+ slug: serializers
6
+ ---
7
+ # Serializers
8
+
9
+ Serializers allow complex objects to be converted to Ruby primitives (`Array` and `Hash` objects),
10
+ which can then be converted to JSON or XML.
11
+
12
+ ## NativeSerializer
13
+
14
+ This serializer uses Rails' native `ActiveModel::Serializers.serializable_hash` method to convert
15
+ records/recordsets to Ruby primitives (`Array` and `Hash`).
16
+
17
+ This is the default serializer, you can configure it using the controller class attributes
18
+ `native_serializer_config` (or `native_serializer_singular_config` /
19
+ `native_serializer_plural_config`):
20
+
21
+ ```ruby
22
+ class Api::MoviesController < ApiController
23
+ include RESTFramework::ModelControllerMixin
24
+
25
+ self.native_serializer_config = {
26
+ only: [:id, :name],
27
+ methods: [:active, :some_expensive_computed_property],
28
+ include: {cast_members: { only: [:id, :name] }},
29
+ }
30
+ end
31
+ ```
32
+
33
+ If you want to re-use a serializer, then you can define it as a standalone class, and you can even
34
+ nest them. You can also define separate configurations for serializing individual records vs
35
+ recordsets using `singular_config` and `plural_config`, respectively.
36
+
37
+ ```ruby
38
+ class Api::MoviesController < ApiController
39
+ include RESTFramework::ModelControllerMixin
40
+
41
+ class CastMemberSerializer < RESTFramework::NativeSerializer
42
+ self.config = { only: [:id, :name], methods: [:net_worth] }
43
+ self.plural_config = { only: [:id, :name] }
44
+ end
45
+
46
+ class MovieSerializer < RESTFramework::NativeSerializer
47
+ self.config = {
48
+ only: [:id, :name],
49
+ include: {cast_members: CastMemberSerializer.new(many: true)},
50
+ }
51
+ self.singular_config = {
52
+ only: [:id, :name],
53
+ methods: [:active, :some_expensive_computed_property],
54
+ include: {cast_members: CastMemberSerializer.new(many: true)},
55
+ }
56
+ end
57
+
58
+ self.serializer_class = MovieSerializer
59
+ end
60
+ ```
@@ -0,0 +1,41 @@
1
+ ---
2
+ layout: default
3
+ title: Filtering / Ordering
4
+ position: 4
5
+ slug: filtering-and-ordering
6
+ ---
7
+ # Filtering and Ordering
8
+
9
+ While you can control the recordset that the API exposes, sometimes you want the user to control the
10
+ records they want to see, or the order of those records. Both filtering and ordering are
11
+ accomplished through what we call filters. To control the filter backends that a controller uses,
12
+ you can either adjust the `filter_backends` controller attribute or you can override the
13
+ `get_filter_backends()` method.
14
+
15
+ ## `ModelFilter`
16
+
17
+ This filter provides basic user-controllable filtering of the recordset using query params. For
18
+ example, a request to `/api/movies?cool=true` could return movies where `cool` is `true`.
19
+
20
+ If you include `ModelControllerMixin` into your controller, `ModelFilter` is included in the filter
21
+ backends by default.
22
+
23
+ ## `ModelOrderingFilter`
24
+
25
+ This filter provides basic user-controllable ordering of the recordset using query params. For
26
+ example, a request to `/api/movies?ordering=name` could order the movies by `name` rather than `id`.
27
+ `ordering=-name` would invert the ordering. You can also order with multiple parameters with a comma
28
+ separated list, like: `ordering=director,-name`.
29
+
30
+ If you include `ModelControllerMixin` into your controller, `ModelOrderingFilter` is included in the
31
+ filter backends by default. You can use `ordering_fields` to controller which fields are allowed to
32
+ be ordered by. To adjust the parameter that the user passes, adjust `ordering_query_param`; the
33
+ default is `"ordering"`.
34
+
35
+ ## `ModelSearchFilter`
36
+
37
+ This filter provides basic user-controllable searching of the recordset using the `search` query
38
+ parameter (adjustable with the `search_query_param`). For example, a request to
39
+ `/api/movies?search=Star` could return movies where `name` contains the string `Star`. The search is
40
+ performed against the `search_fields` attribute, but if that is not set, then the search is
41
+ performed against a configurable default set of fields (`search_columns`).
@@ -0,0 +1,21 @@
1
+ ---
2
+ layout: default
3
+ title: Pagination
4
+ position: 5
5
+ slug: pagination
6
+ ---
7
+ # Pagination
8
+
9
+ For large result sets, you may need to provide pagination. You can configure the paginator for a
10
+ controller by setting the `paginator_class` to the paginator you want to use.
11
+
12
+ ## PageNumberPaginator
13
+
14
+ This is a simple paginator which splits a recordset into pages and allows the user to select the
15
+ desired page using the `page` query parameter (e.g., `/api/movies?page=3`). To adjust this query
16
+ parameter, set the `page_query_param` controller attribute.
17
+
18
+ By default the user can adjust the page size using the `page_size` query param. To adjust this query
19
+ parameter, you can set the `page_size_query_param` controller attribute, or set it to `nil` to
20
+ disable this functionality. By default, there is no upper limit to the size of a page a user can
21
+ request. To enforce an upper limit, set the `max_page_size` controller attribute.
@@ -0,0 +1,144 @@
1
+ {% capture headingsWorkspace %}
2
+ {% comment %}
3
+ Copyright (c) 2018 Vladimir "allejo" Jimenez
4
+
5
+ Permission is hereby granted, free of charge, to any person
6
+ obtaining a copy of this software and associated documentation
7
+ files (the "Software"), to deal in the Software without
8
+ restriction, including without limitation the rights to use,
9
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the
11
+ Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
25
+ {% endcomment %}
26
+ {% comment %}
27
+ Version 1.0.7
28
+ https://github.com/allejo/jekyll-anchor-headings
29
+
30
+ "Be the pull request you wish to see in the world." ~Ben Balter
31
+
32
+ Usage:
33
+ {% include anchor_headings.html html=content anchorBody="#" %}
34
+
35
+ Parameters:
36
+ * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll
37
+
38
+ Optional Parameters:
39
+ * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content
40
+ * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `<a>` tag; you may NOT use `href`, `class` or `title`;
41
+ the `%heading%` and `%html_id%` placeholders are available
42
+ * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available
43
+ * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space
44
+ * anchorTitle (string) : '' - The `title` attribute that will be used for anchors
45
+ * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored
46
+ * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored
47
+ * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content
48
+ * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content
49
+
50
+ Output:
51
+ The original HTML with the addition of anchors inside of all of the h1-h6 headings.
52
+ {% endcomment %}
53
+
54
+ {% assign minHeader = include.h_min | default: 1 %}
55
+ {% assign maxHeader = include.h_max | default: 6 %}
56
+ {% assign beforeHeading = include.beforeHeading %}
57
+ {% assign nodes = include.html | split: '<h' %}
58
+
59
+ {% capture edited_headings %}{% endcapture %}
60
+
61
+ {% for _node in nodes %}
62
+ {% capture node %}{{ _node | strip }}{% endcapture %}
63
+
64
+ {% if node == "" %}
65
+ {% continue %}
66
+ {% endif %}
67
+
68
+ {% assign nextChar = node | replace: '"', '' | strip | slice: 0, 1 %}
69
+ {% assign headerLevel = nextChar | times: 1 %}
70
+
71
+ <!-- If the level is cast to 0, it means it's not a h1-h6 tag, so let's see if we need to fix it -->
72
+ {% if headerLevel == 0 %}
73
+ <!-- Split up the node based on closing angle brackets and get the first one. -->
74
+ {% assign firstChunk = node | split: '>' | first %}
75
+
76
+ <!-- If the first chunk does NOT contain a '<', that means we've broken another HTML tag that starts with 'h' -->
77
+ {% unless firstChunk contains '<' %}
78
+ {% capture node %}<h{{ node }}{% endcapture %}
79
+ {% endunless %}
80
+
81
+ {% capture edited_headings %}{{ edited_headings }}{{ node }}{% endcapture %}
82
+ {% continue %}
83
+ {% endif %}
84
+
85
+ {% capture _closingTag %}</h{{ headerLevel }}>{% endcapture %}
86
+ {% assign _workspace = node | split: _closingTag %}
87
+ {% assign _idWorkspace = _workspace[0] | split: 'id="' %}
88
+ {% assign _idWorkspace = _idWorkspace[1] | split: '"' %}
89
+ {% assign html_id = _idWorkspace[0] %}
90
+
91
+ {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}
92
+ {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
93
+
94
+ <!-- Build the anchor to inject for our heading -->
95
+ {% capture anchor %}{% endcapture %}
96
+
97
+ {% if html_id and headerLevel >= minHeader and headerLevel <= maxHeader %}
98
+ {% capture anchor %}href="#{{ html_id }}"{% endcapture %}
99
+
100
+ {% if include.anchorClass %}
101
+ {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %}
102
+ {% endif %}
103
+
104
+ {% if include.anchorTitle %}
105
+ {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', header }}"{% endcapture %}
106
+ {% endif %}
107
+
108
+ {% if include.anchorAttrs %}
109
+ {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', header | replace: '%html_id%', html_id }}{% endcapture %}
110
+ {% endif %}
111
+
112
+ {% capture anchor %}<a {{ anchor }}>{{ include.anchorBody | replace: '%heading%', header | default: '' }}</a>{% endcapture %}
113
+
114
+ <!-- In order to prevent adding extra space after a heading, we'll let the 'anchor' value contain it -->
115
+ {% if beforeHeading %}
116
+ {% capture anchor %}{{ anchor }} {% endcapture %}
117
+ {% else %}
118
+ {% capture anchor %} {{ anchor }}{% endcapture %}
119
+ {% endif %}
120
+ {% endif %}
121
+
122
+ {% capture new_heading %}
123
+ <h{{ _hAttrToStrip }}
124
+ {{ include.bodyPrefix }}
125
+ {% if beforeHeading %}
126
+ {{ anchor }}{{ header }}
127
+ {% else %}
128
+ {{ header }}{{ anchor }}
129
+ {% endif %}
130
+ {{ include.bodySuffix }}
131
+ </h{{ headerLevel }}>
132
+ {% endcapture %}
133
+
134
+ <!--
135
+ If we have content after the `</hX>` tag, then we'll want to append that here so we don't lost any content.
136
+ -->
137
+ {% assign chunkCount = _workspace | size %}
138
+ {% if chunkCount > 1 %}
139
+ {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %}
140
+ {% endif %}
141
+
142
+ {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %}
143
+ {% endfor %}
144
+ {% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }}