Eleventy, Nunjucks, and shortcode performance/syntax for arbitrarily many items - nunjucks

I'm creating an Eleventy shortcode (for use mostly in markdown) that takes an arbitrary number of arguments and then applies formatting to them before spitting everything out. It vaguely looks like this:
eleventyConfig.addShortcode("theShortcode", function(...args) {
let list = '';
args.forEach( function( listItem) {
list += '<li>' + listItem + '</li>';
});
return list;
});
And then you do something like this for an arbitrary number of items:
{% theShortcode "item1" "item2" "item3" %}
So I'm not convinced this is the most user friendly way of generating this content. At the very least, I suspect in the average case, the item list will be pretty long and become painfully unreadable in the editor. Is there a smarter way to achieve the same result, or a better syntax I might use here?

If all your shortcode does is create a list, why not just write those in normal markdown? Markdown has a syntax for ordered and unordered lists, after all:
- unordered list item 1
- unordered list item 2
- unordered list item 3
1. ordered list item 1
2. ordered list item 2
3. ordered list item 3
If your example was simplified and you actually need to output custom HTML that's not possible in plain markdown, you could use a paired shortcode instead. It can do the same thing as your regular shortcode, but paired shortcodes allow you to write plain content in between the start and end tag, which is easier to read. As an example, here's your list shortcode as a paired shortcode:
eleventyConfig.addShortcode("makelist", content => {
const items = content.split(/\r\n|\r|\n/).filter(Boolean);
const list = items.map(item => `<li>${item}</li>`);
return `<ul>${"\n"}${list.join("\n")}${"\n"}</ul>`;
});
You can add more arguments to the function and pass parameters (like switching between an ordered and an unordered list) in the start tag of the shortcode. Use it like this:
{% makelist %}
list item 1
list item 2
list item 3
{% endmakelist %}

Related

Thymeleaf: Check if list contains a string containing a substring

I have a combined check that needs to happen in Thymeleaf:
List contains an item - can be done as th:if="${#lists.contains(data, '...')}" if you know the exact string
Item contains a substring - When iterating, can be done as th:each="item : *{data}" th:if="${#strings.contains(item,'(')}" e.g. to check for the substring "(" among the items of the list
I need to display a UL tag if the list contains an item containing the substring "(". No iteration, just this combined condition. How do I achieve that in one line?
<ul th:if="..."> <!-- This must be a combined check, no iteration. I don't even want to output the UL if not satisfied -->
</ul>
You can accomplish this with collection selection. Just test if the list size is greater than zero. Something like this will work:
<ul th:if="${#lists.size(data.?[#strings.contains(#this,'(')]) GT 0}">
</ul>
Assume you have a list like this, for testing:
List<String> data = Stream.of("abc", "d(ef", "ghi")
.collect(Collectors.toList());
You can use the following:
<ul th:if="${#strings.contains( #strings.listJoin(data,'') ,'(')}">
bazinga
</ul>
This first concatenates each item in the list into a single string.
It then checks to see if that string contains any ( characters.

CKEditor Plugin - Proper behavior of elementPath

Currently, I have the following HTML content
<span criteria="{"animal":["DOG"]}">abc</span> def <span criteria="{"animal":["CAT"]}">ghi</span>
My purpose is
I wish to know my selected text contain criteria attribute?
If it contains criteria attribute, what is its value?
I run the following code.
editor.on('selectionChange', function( ev ) {
var elementPath = editor.elementPath();
var criteriaElement = elementPath.contains( function( el ) {
return el.hasAttribute('criteria');
});
var array = elementPath.elements;
var arrayLength = array.length;
for (var i = 0; i < arrayLength; i++) {
console.log(i + " --> " + array[i].$.innerHTML);
}
if (criteriaElement) {
console.log("criteriaElement is something");
console.log("criteriaElement attribute length is " + criteriaElement.$.attributes.length);
for (var i = 0; i < criteriaElement.$.attributes.length; i++) {
console.log("attribute is " + criteriaElement.$.attributes[i].value);
}
}
});
Test Case 1
When I select my text abc def as follow
I get the following logging
0 --> abc
1 --> <span criteria="{"animal":["DOG"]}">abc</span> def <span criteria="{"animal":["CAT"]}">ghi</span>
criteriaElement is something
criteriaElement attribute length is 1
attribute is {"operator":["DOG"]}
Some doubts in my mind.
I expect there will be 2 elements in elementPath. One is abc, another is def. However, it turns out, my first element is abc (correct), and my second element is the entire text (out of my expectation)
Test Case 2
I test with another test. This time, I select def ghi
I get the following logging
0 --> <span criteria="{"animal":["DOG"]}">abc</span> def <span criteria="{"animal":["CAT"]}">ghi</span>
Some doubts in my mind
Why there is only 1 element? I expect there will be 2 elements in elementPath. One is def, another is ghi.
Although Test Case 1 and Test Case 2 both contain element with entire text, why in Test Case 2, elementPath.contains... returns nothing?
Elementspath is not related to the selection in that way. It represent the stack of elements under the the caret. Imagine a situation like this where [] represents the selection and | represents the caret:
<ul>
<li>Quux</li>
<li>F[oo <span class="bar">Bar</span> <span class="baz">Ba|]z</span></li>
<li>Nerf</li>
</ul>
Your selection visually contains the text "oo Bar Ba" and your caret is in between a and z. At that time, the elementspath would display "ul > li > span". The other span element "bar" is a sibling of the span element "baz" and is thus not displayed, only ascendants are displayed.
You could think of it like that the caret can only exist inside a html TEXT_NODE and the elementspath displays the ascendants of that text node.
What are you trying to eachieve? To display the data in the current selection? Why? Where do you want it to show? How and why do you want it to show? I'm guessing that there is a different way of fillind the requirement that you have than with using the elementspath (I'm think this might be and XY problem).
Too long to be a comment: If your toolbar button action targets elements with the criteria attribute - what if there is one span with a criteria attribute and 1 without? Does their order matter? What if there are two spans with a criteria attribute? What if they are nested like this: <p>F[oo <span criteria="x">Bar <span criteria="y">Ba|]z </span>Quux </span>Xyzzy</p> - the targeting will be difficult. I would suggest that you add a small marker to the elementspath if an element has the attribute, than clicking the marker or rightclicking the element you could edit/view the criteria. You could even visually indicate spans with the attribute within the editor by customizing editor.css with a rule like span[criteria]{ color: red; }.

Capybara writing drop down's options texts into an array

I'd like to put a dropdown list's options into an array generically in capybara. After the process I'm expecting to have an arrray of strings, containing all dropdown options. I've tried the code below but the length of my array stays 1 regardless of what the option count is.
periods = Array.new()
periods = all('#MainContent_dd')
print periods.length
The problem is that all('#MainContent_dd') returns all elements that have the id MainContent_dd. Assuming this is your dropdown and ids are unique, it is expected that the periods.length is 1 (ie periods is the select list).
What you want to do is get the option elements instead of the select element.
Assuming your html is:
<select id="MainContent_dd">
<option>Option A</option>
<option>Option B</option>
<option>Option C</option>
</select>
Then you can do:
periods = find('#MainContent_dd').all('option').collect(&:text)
p periods.length
#=> 3
p periods
#=> ["Option A", "Option B", "Option C"]
What this does is:
find('#MainContent_dd') - Finds the select list that you want to get the options from
all('option') - Gets all option elements within the select list
collect(&:text) - Collects the text of each option and returns it as an array
#JustinCo's answer has a problem if used driver isn't fast: Capybara will make a query to driver for every invocation of text. So if select contains 200 elements, Capybara will make 201 query to browser instead of 1 which may be slow.
I suggest you to do it using one query with Javascript:
periods = page.execute_script("options = document.querySelectorAll('#MainContent_dd > option'); texts=[]; for (i=0; i<options.length; i++) texts.push(options[i].textContent); return texts")
or (shorter variant with jQuery):
periods = page.evaluate_script("$('#MainContent_dd').map(function() { return $(this).text() }).get()")

Store result of Jinja filter

The basics of what I am trying to do is to use the 'random' filter to choose a random item from my list but then I want to use that randomly chosen item in multiple locations.
How do I set the result of a filter to a variable that I can use in multiple locations.
If I call the 'random' filter multiple times there is little chance they will be the same.
Essentially what I want to do:
{% set image = {{ images | random }} %}
obviously this doesnt work.
Use the filter without {{ }} delimiters.
{% set image = images|random %}
Jinja stores globals and filters in two different namespaces (dictionaries), which prevents them from being used interchangeably.
It's not working on loop, I had used this code:
{% set result = result | replace('x','y') %}

Use Nokogiri to get all nodes in an element that contain a specific attribute name

I'd like to use Nokogiri to extract all nodes in an element that contain a specific attribute name.
e.g., I'd like to find the 2 nodes that contain the attribute "blah" in the document below.
#doc = Nokogiri::HTML::DocumentFragment.parse <<-EOHTML
<body>
<h1 blah="afadf">Three's Company</h1>
<div>A love triangle.</div>
<b blah="adfadf">test test test</b>
</body>
EOHTML
I found this suggestion (below) at this website: http://snippets.dzone.com/posts/show/7994, but it doesn't return the 2 nodes in the example above. It returns an empty array.
# get elements with attribute:
elements = #doc.xpath("//*[#*[blah]]")
Thoughts on how to do this?
Thanks!
I found this here
elements = #doc.xpath("//*[#*[blah]]")
This is not a useful XPath expression. It says to give you all elements that have attributes that have child elements named 'blah'. And since attributes can't have child elements, this XPath will never return anything.
The DZone snippet is confusing in that when they say
elements = #doc.xpath("//*[#*[attribute_name]]")
the inner square brackets are not literal... they're there to indicate that you put in the attribute name. Whereas the outer square brackets are literal. :-p
They also have an extra * in there, after the #.
What you want is
elements = #doc.xpath("//*[#blah]")
This will give you all the elements that have an attribute named 'blah'.
You can use CSS selectors:
elements = #doc.css "[blah]"

Resources