Skip to content

Issue 158 nested attributes 3 #265

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 3 commits 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
48 changes: 47 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,51 @@ 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 the names of these attributes under the `asciidoctor.merge_attributes` key in `_config.yml` as a yaml array or comma-separated string.
The object value of each named attribute will be deep-merged with any value provided in a page header to generate the Jekyll page variable value.
The attributes named in the merge_attributes key are provided to the page as strings generated by Ruby's `.to_s` operator.
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:
attributes:
page-complex:
foo1: bar
foo2:
bar1:
- x
- y
bar2: baz
merge_attributes:
- page-complex
----

[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
9 changes: 9 additions & 0 deletions lib/jekyll-asciidoc/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ def initialize config
attrs = asciidoctor_config[:attributes] = compile_attributes asciidoctor_config[:attributes],
(compile_attributes asciidoc_config['attributes'],
((site_attributes.merge ImplicitAttributes).merge DefaultAttributes))
merge_attributes = asciidoctor_config[:merge_attributes] || []
merge_attributes = (merge_attributes.split ',').collect(&:strip) if ::String === merge_attributes
merge_attributes.each_with_object(asciidoctor_config[:merge_attributes] = {}) do |key, m_attr|
next unless (attrs.key? key) && !(val = attrs[key]).nil?

sval = val.to_s
attrs[key] = sval + '@'
m_attr[key] = [val, sval]
end
if (imagesdir = attrs['imagesdir']) && !(attrs.key? 'imagesoutdir') && (imagesdir.start_with? '/')
attrs['imagesoutdir'] = ::File.join dest, (imagesdir.chomp '@')
end
Expand Down
49 changes: 45 additions & 4 deletions lib/jekyll-asciidoc/integrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,32 @@ def integrate document, collection_name = nil
%(Document '#{document.relative_path}' does not have a valid revdate in the AsciiDoc header.)
end

# key => [original, stringified]
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]) ?
(parse_and_deep_merge merge_attributes[implicit_var], 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 ?
(parse_and_deep_merge merge_attributes[key], 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 +171,33 @@ def parse_yaml_value val
end
end
end

def parse_and_deep_merge old_info, new
return (parse_yaml_value new) unless old_info

if old_info[1] == new
old_info[0]
else
deep_merge old_info[0], (parse_yaml_value new)
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
43 changes: 43 additions & 0 deletions spec/fixtures/merge_attributes/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
plugins:
- jekyll-asciidoc

asciidoc:
implicit_page_variables:
- implicit1
- implicit2

asciidoctor:
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
merge_attributes:
- page-attr
- page-complex
- implicit1
- implicit2
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_attributes layout.</p>
</footer>
</body>
</html>
26 changes: 26 additions & 0 deletions spec/fixtures/merge_attributes/merge.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
= 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!

{page-attr}

{page-complex}

{implicit1}

{implicit2}
11 changes: 11 additions & 0 deletions spec/fixtures/merge_attributes/no-merge.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
= Page with merge_attributes, no overrides

Ludicrous content!

{page-attr}

{page-complex}

{implicit1}

{implicit2}
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