Skip to content

#158 implement yaml page variable merging #261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ For a detailed view of what has changed, refer to the {url-repo}/commits/master[
* Convert documentation from README.adoc to an Antora component in the Asciidoctor docs site. (#238)
* Minimum asciidoctor version 2.0, ruby 2.4, jruby 9.2.7.
Earlier ruby versions don't seem to be supported on CI infrastructure. (#237)
* Explain liquid include interactions (docs) (#223)
* Explain liquid include interactions (docs). (#223)
* Explain asciidoctor-diagram data-uri and svg options (docs).
Based on (#217)
Based on (#217).
* Provide asciidoc_docinfo liquid filter to allow including docinfo content in layouts. (#164)
* Correct excerpt hook handling so excerpts can be used with other Jekyll plugins. (#230)
* Enhance test setup to allow multiple `_config.yml` files per fixture.
* Switch to GitHub Actions based CI: drop travis and appveyor. (#240)
* Implement `implicit_page_variables` asciidoc key support (#207)
* Support Jekyll `unpublished` key overrriding AsciiDoc `page-published` attribute (#257)
* Implement `implicit_page_variables` asciidoc key support. (#207)
* Support Jekyll `unpublished` key overrriding AsciiDoc `page-published` attribute. (#257)
* Document existing behavior around yaml page attributes and implement a way to merge config and page attributes for page variables. (#158)

== 3.0.0.beta.2 (2019-06-03) - @mojavelinux

Expand Down
2 changes: 2 additions & 0 deletions docs/modules/ROOT/pages/global-page-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ asciidoctor:
sectanchors: ''
----

== Global attribute override behavior

By default, an attribute value defined in `_config.yml` overrides the same attribute set in the front matter or header of a document.
For example, if you set `page-layout` in `_config.yml`, you won't be able to set it per page.

Expand Down
45 changes: 44 additions & 1 deletion docs/modules/ROOT/pages/page-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Any AsciiDoc attribute defined globally or in the AsciiDoc document header whose name begins with `page-` (<<customizing-the-page-prefix, cf this>>) gets promoted to a {url-jekyll-variables}[page variable].
The part of the name after the `page-` prefix is lowercased and used as the variable name (e.g., page-LayOut becomes layout).
The variable value is the {url-yaml}[YAML] data (single-line form) translation of the attribute string value.
Since such a global attribute set in `_config.yml` is already parsed as yaml, its value as a page variable is already naturally a Ruby object.
For attributes set in the document header, the variable value is the {url-yaml}[YAML] data (single-line form) translation of the attribute string value.

Since the Asciidoc string attribute value is processed into YAML data as a page variable, you can build nested data structure using the inline YAML syntax.
For example, here's how you can assign a value to the `page.header.image` page variable:
Expand All @@ -27,6 +28,48 @@ For longer expressions that are inconvenient to write on one line, line continua
----
Note that this is still in yaml single-line syntax.

== Merging attribute values

If an attribute that becomes a page variable is defined both in `_config.yml` and the page header, one value or the other is used, depending on the xref:global-page-attributes.adoc#global-attribute-override-behavior[override setting] on the global value.
For values that parse to complex objects it can be useful to merge global and page objects.
Specify such objects under the `asciidoctor.merge_attributes` key in `_config.yml`.
The object value provided there will be deep-merged with any value provided in a page header to generate the Jekyll page variable value.
The deep-merge only takes account of hash keys; unlike some deep-merge implementations array values are simply replaced.
Also, there is no way to remove a key, although it can be set to nil.

For instance:

[source,yaml]
----
asciidoctor:
merge_attributes:
page-complex:
foo1: bar
foo2:
bar1:
- x
- y
bar2: baz
----

[source,adoc]
----
= merge_attributes demo
:page-complex: {foo1: not bar!, \
foo2: { \
bar1: [z] \
}}
----

yields (Ruby object `.to_s` representation)

[source,yaml]
----
{"foo1"=>"not bar!", "foo2"=>{"bar1"=>["z"], "bar2"=>"baz"}}
----

== Tips and tricks

To define a page attribute name that contains multiple words, use either a hyphen or underscore character to connect the words.

[source,asciidoc]
Expand Down
1 change: 1 addition & 0 deletions lib/jekyll-asciidoc/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def initialize config
attrs['imagesoutdir'] = ::File.join dest, (imagesdir.chomp '@')
end
attrs[%(#{asciidoc_config['page_attribute_prefix']}published)] = '' if config['unpublished']
asciidoctor_config[:merge_attributes] = {} unless asciidoctor_config[:merge_attributes]
asciidoctor_config.extend Configured
end
end
Expand Down
38 changes: 34 additions & 4 deletions lib/jekyll-asciidoc/integrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,31 @@ def integrate document, collection_name = nil
%(Document '#{document.relative_path}' does not have a valid revdate in the AsciiDoc header.)
end

merge_attributes = document.site.config['asciidoctor'][:merge_attributes]

implicit_vars = document.site.config['asciidoc']['implicit_page_variables']
implicit_vars = (implicit_vars.split ',').collect(&:strip) if ::String === implicit_vars
implicit_vars&.each do |implicit_var|
val = doc.attributes[implicit_var]
data[implicit_var] = ::String === val ? (parse_yaml_value val) : val if val
if doc.attributes.key? implicit_var
val = ::String === (val = doc.attributes[implicit_var]) ?
(deep_merge merge_attributes[implicit_var], (parse_yaml_value val)) : val
else
val = merge_attributes[implicit_var]
end
data[implicit_var] = val if val
end

page_attr_prefix = document.site.config['asciidoc']['page_attribute_prefix']
no_prefix = (prefix_size = page_attr_prefix.length) == 0
adoc_data = doc.attributes.each_with_object({}) do |(key, val), accum|
if no_prefix || ((key.start_with? page_attr_prefix) && (key = key[prefix_size..-1]))
accum[key] = ::String === val ? (parse_yaml_value val) : val
if (short_key = shorten key, page_attr_prefix, no_prefix, prefix_size)
accum[short_key || key] = ::String === val ?
(deep_merge merge_attributes[key], (parse_yaml_value val)) : val
end
end
merge_attributes.each do |(key, val)|
if (short_key = shorten key, page_attr_prefix, no_prefix, prefix_size)
adoc_data[short_key] = val unless adoc_data.key? short_key
end
end
data.update adoc_data unless adoc_data.empty?
Expand Down Expand Up @@ -157,6 +170,23 @@ def parse_yaml_value val
end
end
end

# Simple deep merge implementation that only merges hashes.
def deep_merge old, new
return new unless old
return old unless new

old.merge new do |_, oldval, newval|
(::Hash === oldval) && (::Hash === newval) ?
deep_merge(oldval, newval) : newval
end
end

# Is there a way to make this a closure so only the key is passed?
def shorten key, prefix, no_prefix, prefix_size
key if no_prefix
(key.start_with? prefix) && (key[prefix_size..-1])
end
end
end
end
38 changes: 38 additions & 0 deletions spec/fixtures/merge_attributes/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins:
- jekyll-asciidoc

asciidoc:
implicit_page_variables:
- implicit1
- implicit2

asciidoctor:
merge_attributes:
page-attr:
one: one-value
two:
two-sub-one:
- a
- b
- c
two-sub-two:
two-sub-two-sub-one: 221-value
two-sub-two-sub-two: 222-value
two-sub-three:
two-sub-three-sub-one: 231-value
three: three-value
page-complex:
foo1: bar
foo2:
bar1:
- x
- y
bar2: baz
implicit1:
one:
- a
- b
implicit2:
one:
- a
- b
30 changes: 30 additions & 0 deletions spec/fixtures/merge_attributes/_layouts/default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ page.title }}</title>
</head>
<body>
<article class="merge-attributes">
<header>
<p>{{ page.attr.one }}</p>
<p>{{ page.attr.two.two-sub-one[0]}}</p>
<p>{{ page.attr.two.two-sub-one[1]}}</p>
<p>{{ page.attr.two.two-sub-one[2]}}</p>
<p>{{ page.attr.two.two-sub-two.two-sub-two-sub-one}}</p>
<p>{{ page.attr.two.two-sub-two.two-sub-two-sub-two}}</p>
<p>{{ page.attr.two.two-sub-three.two-sub-three-sub-one}}</p>
<p>{{ page.attr.three }}</p>
<p>{{ page.complex }}</p>
<p>{{ page.implicit1 }}</p>
<p>{{ page.implicit2 }}</p>
</header>
<div class="post-content">
{{ content }}
</div>
</article>
<footer>
<p>Footer for merge_attribtes layout.</p>
</footer>
</body>
</html>
18 changes: 18 additions & 0 deletions spec/fixtures/merge_attributes/merge.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
= Page with merge_attributes and overrides
:page-attr: {one: one-value-override, \
two: { \
two-sub-one: \
[d, e], \
two-sub-two: \
{ two-sub-two-sub-one: 221-value-override } \
} \
}
:page-complex: {foo1: not bar!, \
foo2: { \
bar1: [z] \
}}
:implicit1: {one: [c,d]}
:implicit2: {one: [c,d]}
:page-implicit2: {one: [e,f]}

Ludicrous content!
3 changes: 3 additions & 0 deletions spec/fixtures/merge_attributes/no-merge.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
= Page with merge_attributes, no overrides

Ludicrous content!
45 changes: 45 additions & 0 deletions spec/jekyll-asciidoc_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1390,4 +1390,49 @@
(expect ::File).to exist file
end
end

describe 'merge_attributes' do
use_fixture :merge_attributes

before :each do
site.process
end

it 'should merge yaml properly' do
file = output_file 'merge.html'
(expect ::File).to exist file
contents = ::File.read file
header = (contents.match %r/<header>.*<\/header>/m)[0]
(expect header).to include '<p>one-value-override</p>'
(expect header).to include '<p>d</p>'
(expect header).to include '<p>e</p>'
(expect header).to include '<p>221-value-override</p>'
(expect header).to include '<p>222-value</p>'
(expect header).to include '<p>231-value</p>'
(expect header).to include '<p>three-value</p>'
# This verifies the example in the doc works as advertised:
(expect header).to include '{"foo1"=>"not bar!", "foo2"=>{"bar1"=>["z"], "bar2"=>"baz"}}'
# implicit page variables
(expect header).to include '{"one"=>["c", "d"]}'
(expect header).to include '{"one"=>["e", "f"]}'
end

it 'configured value should be present with no overrides' do
file = output_file 'no-merge.html'
(expect ::File).to exist file
contents = ::File.read file
header = (contents.match %r/<header>.*<\/header>/m)[0]
(expect header).to include '<p>one-value</p>'
(expect header).to include '<p>a</p>'
(expect header).to include '<p>b</p>'
(expect header).to include '<p>c</p>'
(expect header).to include '<p>221-value</p>'
(expect header).to include '<p>222-value</p>'
(expect header).to include '<p>231-value</p>'
(expect header).to include '<p>three-value</p>'
# implicit page variables
(expect header).to include '{"one"=>["a", "b"]}'
(expect header).to include '{"one"=>["a", "b"]}'
end
end
end