How do I mark a filter as being safe? - nunjucks

I am trying to set up a syntax highlighting filter for nunjucks using highlight.js. It seems pretty easy to do. In my elevnety.js file I included:
const hljs = require('highlight.js');
eleventyConfig.addFilter('highlight', function(txt) {
return hljs.highlightAuto(txt).value;
});
It appears that highlight.js is a safe filter and will properly escape it's contents and add markup to control the highlighting, so there is nothing else to do.
In my njk page I try to use this with
{% filter highlight %}
<xmlstuff>
<myelements attr1="foo" />
</xmlsfuff>
{% endfilter %}
The highlighting markup is being generated correctly, but the whole result is being escaped (by nunjucks perhaps) so the resulting page renders all the markup code. Here is what is getting added to the output html page:
<span class="hljs-tag">&lt;<span class="hljs-name">xmlstuff</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">myelements</span> <span class="hljs-attr">attr1</span>=<span class="hljs-string">"foo"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">xmlsfuff</span>&gt;</span>
I know that nunjucks has a safe filter to prevent this from happening, but I don't know how to apply that to say that my filter block doesn't need escaping, and I could not find anything in the documentation. I tried a number of approaches, but they all failed:
{% filter highlight | safe %}
<xmlstuff>
<myelements attr1="foo" />
</xmlsfuff>
{% endfilter %}
{% filter highlight %}
<xmlstuff>
<myelements attr1="foo" />
</xmlsfuff>
{% endfilter | safe %}
{{ {% filter highlight %}
<xmlstuff>
<myelements attr1="foo" />
</xmlsfuff>
{% endfilter %} | safe }}
Is there any way to mark this filter block as safe?

Try to apply env.filters.safe before output.
var nunjucks = require('nunjucks');
var env = nunjucks.configure();
env.addFilter('safeFilter', str => env.filters.safe(str));
env.addFilter('unsafeFilter', str => str);
var html = env.renderString(
`{{ str | safeFilter }}\n{{ str | unsafeFilter }}`,
{str: '<h1>Hello</h1>'}
);
console.log(html);

#aikon-mogwai posted the correct answer. However for eleventy a bit more is required because the filter needs to be set up in the eleventy.js file and we need to get access to the nunjucks environment. I'll add my complete solution here for posterity.
I does not look like eleventy provides access to the nunjucks environment, so we need to create one and set it to override the existing environment. After that it is pretty well what he said:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy('src/images')
/*
* Create and register a Nunjucks environment, just so we
* can get access to the safe filter.
*/
let Nunjucks = require("nunjucks");
let nunjucksEnvironment = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader("src/_includes"), { }
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
/*
* Set up a syntax highlighting filter for code blocks
*/
const hljs = require('highlight.js');
eleventyConfig.addNunjucksFilter('highlight', function(txt, lang) {
var txt2;
if (lang == undefined)
txt2 = hljs.highlightAuto(txt).value;
else
txt2 = hljs.highlight(lang, txt).value;
return nunjucksEnvironment.filters.safe(txt2);
});
return {
dir: { input: 'src', output: 'dist', data: '_data' },
passthroughFileCopy: true,
templateFormats: ['njk', 'md', 'css', 'html', 'yml'],
htmlTemplateEngine: 'njk'
}
}

I achieved this with Eleventy#1.0.0-beta.8 like this:
eleventyConfig.addNunjucksFilter('doThing', function (value) {
return this.env.filters.safe(doThing(value));
})

Related

After browser back button load last search

I have own plugin in October CMS what have a onFilter() function whats return and displays partials with data. When user click on name it redirect him to a detail page. And when user click back button in browser I want to display his last search.
I tried something like Session::push('data', $data) in onFilter() method and Session::get('data') in onInit() but it didnt work. $data is a list of pubs.
Had anyone same problem?
Edit
public function onFilter()
{
$result =
Lounge::filterLounge($categories, $tags, $regions, $paidIds, $price_from, $search);
Session::put('nameFilter', $result);
return [
'#list' => $this->renderPartial('loungelist::list.htm', [
'list_data' => $result])
];
}
public function getNameFilter() {
$nameFilter = Session::get('nameFilter');
return $nameFilter;
}
In partial .htm
{% set list_data = __SELF__.getNameFilter() %}
{% for lounge in list_data %}
{{ lounge.name }}
{% endfor %}
As #mwilson mentions I would use window.history on the front end with the pushstate() function so that when each filter is changed you push state including query strings before firing to php to get the filtered content. I have done this before and works very well.
You should post more code for us to be more helpful. I am assuming you are using a component. Are you using ajax? Session will do the job.
In your component.php put onNameFilter() event you can push the data into the session with Session::put('nameFilter' $data);. I suggest using more specific labels for your events and keys so I chose 'nameFilter'.
You will want to use a method in your component.php to call the session.
public function getNameFilter() {
$nameFilter = Session::get('nameFilter');
return $nameFilter;
}
Now in your partial.htm you can set the name filter data and access it as long as it is in the session:
{% set nameFilterData = __SELF__.getNameFilter() %}
EDIT TO SHOW REFLECTED CODE
I don't understand how it works the first time. What does your CMS Page look like? How do you show the filter the "first time"?
Your CMS Page has {% component 'something' %} right? Then in your default.htm file you have {% partial __SELF__~'::list %}?
In your partial you will need to display the list_data. Does this show anything?
{% for list in list_data %}
{{ list_data.name }}
{% endfor %}

Shopify Liquid Search.Results - How to Sort by Barcode?

we have been struggling with this problem for weeks and still have not found a solution.
We have a Shopify store with 2000 products and 300 vendors. Each vendor has a "ranking number" which we have assigned to them and entered into the "barcode" field in the variants of every single product.
The goal is that we want Shopify to be able to sort our search results by the Barcode using the liquid "sort" tag like so:
{% assign foo = search.results | sort:'title' %}
However, when we attempt to use this syntax to sort by "barcode" it doesn't return any results
{% assign foo = search.results | sort:'barcode' %}
We have attempted to use 'sort' with all the following arguments but none of them work:
{% assign foo = search.results | sort:'product.barcode' %}
{% assign foo = search.results | sort:'item.barcode' %}
{% assign foo = search.results | sort:'item.variants.first.barcode' %}
{% assign foo = search.results | sort:'items.variants.variant.barcode' %}
etc, but none of them work.
If ANYONE can point us in the right direction for which argument/syntax to use to sort our search results by variant.barcode, it would be MUCH APPRECIATED!!!!!
Thanks
In terms of native functionality you're out of luck.
You can do something like this though by rolling your own.
Manually sort your collections by vendor rank
Limit your search to collection results by creating your own search function
or
creat your own search function but make it a "filter" on your current collection and leave the native search alone.
Create Your Own Search:
Put your item results cell in a snippet. collection-product.liquid
Include that snippet in
your collecton liquid.
Make a new collection template
(collection.search-results.liquid)
collection.search-results.liquid looks something like the following. Your pagination size should match the pagination size of your main collection.
{% layout none %}
{% paginate collection.products by 50 %}
<div class="search-results">
{% for product in collection.products %}
{% include 'collection-product' %}
{% endfor %}
</div>
{% endpaginate %}
Now your search becomes an input box with some javascript. The fun part. The script below was pulled from a working site. I've trimmed it up somewhat to hopefully make the bones of it clearer but it probably won't run as is. I've removed references to adding a search because for your purposes you want the native custom search.
As to why not just sort the results. You could use this concept to do that but it returns results a page at a time. In order to custom search all the results you'd have to accumulate the results and sort them when the search is finished. Search time then depends heavily on the size of the collection and the user's network speed.
var defaultView = $(".filters-off"); // add this class to your main collection wrapper
var facetView = $("#filter_target"); // <div id="filter_target"></div> below your main collection wrapper
var currentRequest = 0;
function filterCollection(term){
//[[t1, t2], [t3,t4]]
// console.log('applying '+ JSON.stringify(facets));
var anyResults = false;
var resultsPage=0;
var PAGE_SIZE = 50;
var productsFound = 0;
var collectionPath = location.pathname;
console.log('get from '+ collectionPath);
currentRequest = new Date().getTime();
var viewLink = collectionPath+'?view=search-results'; // same as your layout none template
function applyFilter(myRequest){
resultsPage++;
if(resultsPage > 20) return false; // arbitrary abort for too many results
if(resultsPage > 1) viewLink+='&page='+resultsPage;
return $.get(viewLink).then(function(page){
if(currentRequest != myRequest){
console.log('mid abort');
return false;
}
var pageProducts = $("div[data-tags]", page); //some markup you added to collection-product snippet to help plucking the product elements
if(!pageProducts.length) return false;
console.log('found: '+ pageProducts.length);
var filteredProducts = pageProducts.filter(function(){
if($(this).text().indexOf(term) != -1) return true; // search the returned text
if($(this).attr('data-keywords').indexOf(term) != -1) return true;
return false;
});
if(filteredProducts.length){
if(!anyResults){
anyResults = true;
toggleView(true);
}
filterView.append(filteredProducts);
productsFound+= filteredProducts.length;
}
return (pageProducts.length == PAGE_SIZE && currentRequest == myRequest) ? applyFilter(myRequest) : false;
}).then(function(proceed){
if(currentRequest == myRequest){
if(!anyResults){
toggleView(false, true);
}
}
});
}
applyFilter(currentRequest);
}
function toggleView (showFacets, showNoGo){
facetView.empty();
$(".filter-progress").empty();
if(showFacets) {
defaultView.add('.pagination').hide();
}else {
if(!showNoGo){
defaultView.add('.pagination').show();
}else {
$(".no-facets").clone(true).appendTo(facetView).show(); // .no-facets is normally hidden chunk that allows for easy internationaliztion of "No results found" type messages
}
}
};

Nunjucks if statement

I am currently investigating if we can make a query string var work within a nunjucks if statement
The below hardcoded value works "5" and brings back the expected result:
{% block content %}
{% for client in clients %}
{% if client.id == 5 %}
{{client.client}}
{% endif %}
{% endfor %}
{% endblock %}
I have the current page id from the url stored in a variable but i can't seem to replace "5" with the actual var
Is this possible?
Edit added JQuery
$(document).ready(function () {
$.urlParam = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
return results[1] || 0;
}
var newid = $.urlParam('id');
alert(newid);
});
Thanks a lot of any time spent.
TCP

OctoberCMS Builder Plugin to Show Data on Front End

This my first time to use OctoberCMS, and they provide the Builder Plugin in order to help the developer builds plugin in minutes.
I try to use this plugin to show the data on the front end especially when using Components > Builder > Record list, but the documentation didn't give enough example to get data from some fields. The example on the internet is just show how to get the data from one field.
My code is shown below:
[builderList]
modelClass = "Budiprasetyo\Employees\Models\Employee"
scope = "-"
displayColumn = "name"
noRecordsMessage = "No records found"
detailsPage = "-"
detailsUrlParameter = "id"
pageNumber = "{{ :page }}"
in my case, I want to get the data not only "name" field, but I also want to add "email", "facebook" fields.
I have tried to make it into:
displayColumn = "name", "email", "facebook"
but it returns no data shown and I have tried to make it into array:
displayColumn = ["name", "email", "facebook"]
and it's the same result, no data is shown.
I appreciate any helps, thank you.
Actually I don't like to use plugins' components. Sometimes it doesn't retrieve any data. I don't know why maybe it's just because of wrong using.
Anyway, The best way to retrieve the data from your plugin is to navigate to the code section and then write onStart() function and start to retrieve to data.
function onStart()
{
$data = \Authorname\Pluginname\Models\Model::find(1);
$this['data'] = $data;
}
And this way you'll have a data variable in the markup section.
On the frontend cms page, you need to replace the component call with your custom partial. Your partial file should look something like this:
*podcastList is the component name
{% set records =podcastList.records %}
{% set displayColumn =podcastList.displayColumn %}
{% set noRecordsMessage =podcastList.noRecordsMessage %}
{% set detailsPage =podcastList.detailsPage %}
{% set detailsKeyColumn =podcastList.detailsKeyColumn %}
{% set detailsUrlParameter =podcastList.detailsUrlParameter %}
{% for record in records %}
<span class="date">{{ attribute(record, 'date') }}</span>
<span class="title">{{ attribute(record, 'title') }}</span>
<p>{{ attribute(record, 'description') }}</p>
{% endfor %}
Obviously, this is simplified version, but you get the point. You can easily define more columns without touching the page component settings.

Drupal 8 img as backgroundimage

I'm making my own drupal 8 theme. This is the first time I use drupal. But I have the following problem: I would like to show the article's image as a background image. In Drupal 7 (I have found) you could do that with the following preprocessor, but how do you achieve this in drupal 8?
function THEME_preprocess_page(&$vars) {
$node = menu_get_object('node');
if (!empty($node) && !empty($node->field_background_image)) {
$image = file_create_url($node->field_background_image[LANGUAGE_NONE][0]['uri']);
drupal_add_css('div#content {background: url(' . $image . ') no-repeat scroll center center / cover #FCFCFC; }', array('type' => 'inline'));
}
}
Thank you
You can get an image field in a background by just doing some tweak with twig.
In a Node-template.html.twig you can get this like.
{% set background_image = node.yourimage_field|striptags|trim %}
OR if the above line does not help then just replace node.yourimage_field|striptags|trim with content.youimage_field|striptags|trim OR you can check the array path to variable by just enabling the module deval and then in your node template file type {{ kint(content) }}
OR {{ kint(node) }}
<div style="background-image:url({{ background_image }});"></div>
In a views-template.html.twig you can do some like this.
{% set background_image = fields.yourimage_field.content|striptags|trim %}
<div style="background-image:url({{ background_image }});"></div>
And That's it.
Adding inline css or js can be achived by adding an '#attached' key to your render array.
$elemnt['#attached']['css_inline'] = // Your css
See https://www.drupal.org/node/2391025
If you use twig for templating you can pass the image url as a variable with th $vars array and use it in in your template.
Anyway, drupal_add_css and drupal_add_js function aren't availablein Drupal 8
There are lots of contributed modules that allow an image field to be rendered as a background image. Using these modules would likely not require any custom code.
My personal favorite is Layout BG. Here is a page with links to several other modules that make it easy to render an image as a background image using a Field Formatter: https://www.drupal.org/docs/contributed-field-formatters/image#s-background-image
You have to write this code in your theme_name.theme file. It is the same as template.php in Drupal 7
function THEME_preprocess_page(&$vars) {
$node = Drupal::request()->attributes->get('node');
if($node){
if ($node->bundle() == "articles")) { // Content type check
$uri = $node->field_background_image->entity->getFileUri();
$image = file_create_url($uri); // Creating file path from uri
$vars['articles'] = true; // Setting value true for current node is articles.
$vars['background_img'] = $image;
}
}
}
Now in your page.twig.html
{% if blog %} // Accessing the blog variable from $vars
<div style="background: url({{ background_img }})"> // Accessing the background_img variable from $vars
// Other content
</div>
{% endif %}
Don't forget to clear cache!
Hope it helps!

Resources