4º. 1er cuatrimestre. Itinerario de Sistemas de la Información. Grado en Ingeniería Informática. ULL
"content": {{ page.content | markdownify | strip_html | jsonify }},
El ejercicio consiste en que añada la capacidad de búsqueda al sitio web contruido en la práctica Introduction to Systems Development
Estos son algunos requisitos:
La entrega de esta práctica se realizará en el mismo repo asociado a la práctica Introduction to Systems Development.
Cree una rama intro2sd
para señalar el punto de entrega de la anterior y haga la entrega de esta tarea en la rama main
.
We will be creating a JSON file in which we will store title
, url
, content
, excerpt
, etc., at building time
$ bundle exec jekyll build
$ head -n 30 _site/assets/src/search.json
[
{
"title": "Clase del Lunes 30/09/2019",
"excerpt": "Clase del Lunes 30/09/2019\n\n", "⇐": " Resumen",
"content": "Clase del Lunes 30/09/2019\n\n\n ...", "⇐ ": "Contenido del fichero"
"url": "/clases/2019/09/30/leccion.html"
},
"...": "..."
]
Véase search.json (protected)
---
layout: null
sitemap: false
---
{% capture json %}
[
{% assign collections = site.collections | where_exp:'collection','collection.output != false' %}
{% for collection in collections %}
{% assign docs = collection.docs | where_exp:'doc','doc.sitemap != false' %}
{% for doc in docs %}
{
"title": {{ doc.title | jsonify }},
"excerpt": {{ doc.excerpt | markdownify | strip_html | jsonify }},
"content": {{ doc.content | markdownify | strip_html | jsonify }},
"url": {{ site.baseurl | append: doc.url | jsonify }}
},
{% endfor %}
{% endfor %}
{% assign pages = site.html_pages | where_exp:'doc','doc.sitemap != false' | where_exp:'doc','doc.title != null' %}
{% for page in pages %}
{
"title": {{ page.title | jsonify }},
"excerpt": {{ page.excerpt | markdownify | strip_html | jsonify }},
"content": {{ page.content | markdownify | strip_html | jsonify }},
"url": {{ site.baseurl | append: page.url | jsonify }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
{% endcapture %}
{{ json | lstrip }}
You can find the source code at /ULL-MFP-AET/ull-mfp-aet.github.io/main/assets/src/search.json
layout: null
: To disable layout in Jekyll.sitemap: false
:
robots.txt
, a URL exclusion protocol. We can use the front-matter to set the sitemap
property to false
{% capture json %} ... {% endcapture %}
Captures the string inside of the opening and closing tags and assigns it to a variable. Variables that you create using capture are stored as strings.{{ json | lstrip }}
:
{{ }}
and are denoted by a pipe character |
.{% assign collections = site.collections ...
site.collections
: Collections are also available under site.collections
. Posts are considered a collections by Jekyll.where_exp:'collection','collection.output != false'
site.collections
is an array. With where_exp
we select all the objects in the array with the elements for which the attribute collection
has its output
attribute to true
.output
attribute of a collection controls whether the collection’s documents will be output as individual files.site.html_pages
: A subset of site.pages
listing those which end in .html
.Use the Liquid Playground to test the Liquid expressions above. The lower left panel is to enter a JSON holding variables that can be accesed in the upper left panel by its name.
"content": {{ page.content | markdownify | strip_html | jsonify }},
page.content
el contenido de la página todavia sin renderizar (se supone que es fundamentalmente markdown, pero puede contener yml en el front-matter, html, scripts, liquid, etc.)markdownify
: Convert a Markdown-formatted string into HTML.jsonify
: If the data is an array or hash you can use the jsonify filter to convert it to JSON.Por ejemplo, supongamos que tenemos estas definiciones en el front-matter de nuestra página:
chuchu: "Cadena **negritas** e *italicas*"
html: "<h1>hello</h1> <b>world</b>"
colors:
- red
- blue
- green
---
y que en el contenido de nuestra página tenemos algo así:
Compara < script>{{ page.chuchu }} </script> con su markdownify: < script>{{ page.chuchu | markdownify }}</script>
Compara < script> {{ page.colors}} </script> con su jsonify: < script>{{ page.colors | jsonify }} </script>
Compara < script>{{page.html}}</script> con su `strip_html` < script> {{ page.html | strip_html }} </script>
Esta es la salida que produce jekyll 4.0.0:
<p>Compara < script>Cadena **negritas** e *italicas* </script> con su markdownify: < script><p>Cadena <strong>negritas</strong> e <em>italicas</em></p>
</script></p>
<p>Compara < script> redbluegreen </script> con su jsonify: < script>["red","blue","green"] </script></p>
<p>Compara < script><h1>hello</h1> <b>world</b></script> con su <code class="highlighter-rouge">strip_html</code> < script> hello world </script></p>
La idea general es que necesitamos suprimir los tags, tanto yml, markdown, HTML, etc. para que no confundan al método de busca. Por eso convertimos el markdown a HTML y después suprimimos los tags HTML. También convertimos el yml a JSON.
Fuente: search.md
La idea es que vamos a escribir una clase JekyllSearch
que implementa la búsqueda.
Debe disponer de un constructor al que se le pasan cuatro argumentos:
jekyll build
)id
del objeto del DOM en la página en la que está el tag input
de la búsquedaid
del objeto del DOM en el que se deben volcar los resultadosurl
del lugar en el que está el deployment (pudiera ser que el site en el que queremos buscar fuera una subcarpeta de todo el site)const search = new JekyllSearch(
'/assets/src/search.json',
'#search',
'#list',
''
);
search.init();
Los objetos JekyllSearch
deben disponer de un método init
que realiza la búsqueda especificada en el elemento del DOM #search
y añade los resultados en en el elemento del DOM #list
---
layout: error
permalink: /search/
title: Search
---
{% capture initSearch %}
<h1>Search</h1>
<form id="search-form" action="">
<label class="label" for="search">Search term (accepts a regex):</label>
<br/>
<input class="input" id="search" type="text" name="search"
autofocus
placeholder="e.g. Promise"
autocomplete="off">
<ul class="list list--results" id="list">
</ul>
</form>
< script type="text/javascript" src="/assets/src/fetch.js"></script>
< script type="text/javascript" src="/assets/src/search.js"></script>
< script type="text/javascript">
const search = new JekyllSearch(
'{{site.url}}/assets/src/search.json',
'#search',
'#list',
'{{site.baseurl}}'
);
search.init();
</script>
<noscript>Please enable JavaScript to use the search form.</noscript>
{% endcapture %}
{{ initSearch | lstrip }}
autocomplete="off"
autocomplete="nope"
Dado que este valor no es válido para el atributo autocompletar, el navegador no tiene forma de reconocerlo y deja de intentar autocompletarlo.
{{ }}
and are denoted by a pipe character |
.
site.url
vs site.baseurl
You can find the source at ULL-MFP-AET/ull-mfp-aet.github.io/assets/src/search.js
Here are the contents:
class JekyllSearch {
constructor(dataSource, searchField, resultsList, siteURL) {
this.dataSource = dataSource
this.searchField = document.querySelector(searchField)
this.resultsList = document.querySelector(resultsList)
this.siteURL = siteURL
this.data = [];
}
fetchedData() {
return fetch(this.dataSource, {mode: 'no-cors'})
.then(blob => blob.json())
}
async findResults() {
this.data = await this.fetchedData()
const regex = new RegExp(this.searchField.value, 'i')
return this.data.filter(item => {
return item.title.match(regex) || item.content.match(regex)
})
}
async displayResults() {
const results = await this.findResults()
//console.log('this.siteURL = ',this.siteURL)
const html = results.map(item => {
//console.log(item)
return `
<li class="result">
<article class="result__article article">
<h4>
<a href="${item.url}">${item.title}</a>
</h4>
<p>${item.excerpt}</p>
</article>
</li>`
}).join('')
if ((results.length == 0) || (this.searchField.value == '')) {
this.resultsList.innerHTML = `<p>Sorry, nothing was found</p>`
} else {
this.resultsList.innerHTML = html
}
}
// https://stackoverflow.com/questions/43431550/async-await-class-constructor
init() {
const url = new URL(document.location)
if (url.searchParams.get("search")) {
this.searchField.value = url.searchParams.get("search")
this.displayResults()
}
this.searchField.addEventListener('keyup', () => {
this.displayResults()
// So that when going back in the browser we keep the search
url.searchParams.set("search", this.searchField.value)
window.history.pushState('', '', url.href)
})
// to not send the form each time <enter> is pressed
this.searchField.addEventListener('keypress', event => {
if (event.keyCode == 13) {
event.preventDefault()
}
})
}
}
constructor(dataSource, searchField, resultsList, siteURL) {
this.dataSource = dataSource
this.searchField = document.querySelector(searchField)
this.resultsList = document.querySelector(resultsList)
this.siteURL = siteURL
this.data = [];
}
The Document
method querySelectorAll()
returns a static (not live) NodeList
representing a list of the
document’s elements that match the specified group of selectors.
selectors: In CSS, pattern matching rules determine which style rules apply to elements in the document tree. These patterns, are called selectors, may range from simple element names to rich contextual patterns. If all conditions in the pattern are true for a certain element, the selector matches the element. For instance '#search'
and '#list'
are selectors.
All methods getElementsBy*
return a live collection.
Such collections always reflect the current state of the document and auto-update when it changes.
In contrast, querySelectorAll
returns a static collection.
It’s like a fixed array of elements.
init() {
const url = new URL(document.location)
if (url.searchParams.get("search")) {
this.searchField.value = url.searchParams.get("search")
this.displayResults()
}
this.searchField.addEventListener('keyup', () => {
this.displayResults()
// So that when going back in the browser we keep the search
url.searchParams.set("search", this.searchField.value)
window.history.pushState('', '', url.href)
})
// to not send the form each time <enter> is pressed
this.searchField.addEventListener('keypress', event => {
if (event.keyCode == 13) {
event.preventDefault()
}
})
}
(also known as query strings) are a way to structure additional information for a given URL.
Parameters are added to the end of a URL after a ?
symbol, and multiple parameters can be included when separated by the &
symbol.
In our case, we have the search
parameter:
If the URL of your page is https://example.com/?name=Jonathan%20Smith&age=18
you could parse out the name
and age
parameters using:
let params = (new URL(document.location)).searchParams;
let name = params.get('name'); // is the string "Jonathan Smith".
let age = parseInt(params.get('age')); // is the number 18
this.searchField.addEventListener('keyup', () => {
this.displayResults()
// So that when going back in the browser we keep the search
url.searchParams.set("search", this.searchField.value)
window.history.pushState('', '', url.href)
})
The window
object provides access to the browser’s session history through the history
object.
The history.pushState(state, title, url)
method adds a state to the browser’s session history stack.
... // inside init
this.searchField.addEventListener('keyup', () => {
this.displayResults()
// So that when going back in the browser we keep the search
url.searchParams.set("search", this.searchField.value)
window.history.pushState('', '', url.href)
})
The search.json
is not going to change until the next push
fetchedData() {
return fetch(this.dataSource, {mode: 'no-cors'})
.then(blob => blob.json())
}
async findResults() {
this.data = await this.fetchedData()
const regex = new RegExp(this.searchField.value, 'i')
return this.data.filter(item => {
return item.title.match(regex) || item.content.match(regex)
})
}
Read the section Introduction to CORS to know what CORS is.
The mode
option of the fetch()
method allows you to define the CORS mode of the request:
no-cors
prevents the method from being anything other than HEAD
, GET
or POST
, and the headers from being anything other than simple headers.no-cors
assures that JavaScript may not access any properties of the resulting Response
.
The resources downloaded through fetch()
, similar to other resources that the browser downloads, are subject to the HTTP cache.
fetchedData() {
return fetch(this.dataSource).then(blob => blob.json())
}
This is usually fine, since it means that if your browser has a cached copy of the response to the HTTP request, it can use the cached copy instead of wasting time and bandwidth re-downloading from a remote server.
npm install whatwg-fetch --save
bower install fetch --save
Esta imagen muestra los ficheros implicados en este ejercicio dentro de la estructura del sitio de estos apuntes:
$ tree -I _site
.
├── 404.md
├── assets
│ ├── css
│ │ └── style.scss
│ ├── images
│ │ ├── event-emitter-methods.png
│ │ └── ,,,
│ └── src
│ ├── fetch.js ⇐ Polyfill for fetch
│ ├── search.js ⇐ Librería con la Clase JekyllSearch que implementa el Código de búsqueda
│ └── search.json ⇐ Plantilla Liquid para generar el fichero JSON
├── search.md ⇐ Página de la búsqueda. Formulario y script de arranque
├── clases.md
├── _config.yml ⇐ Fichero de configuración de Jekyll
├── degree.md
├── favicon.ico
├── Gemfile
├── Gemfile.lock
├── _includes ⇐ The include tag allows to include the content of files stored here
│ ├── navigation-bar.html
│ └── ...
├── _layouts ⇐ templates that wrap around your content
│ ├── default.html
│ ├── error.html
│ └── post.html
├── _posts ⇐ File names must follow YEAR-MONTH-DAY-title.MARKUP and must begin with front matter
│ ├── ...
│ └── 2019-12-02-leccion.md
├── _practicas ⇐ Folder for the collection "practicas" (list of published "practicas")
│ ├── ...
│ └── p9-t3-transfoming-data.md
├── practicas.md ⇐ {% for practica in site.practicas %} ... {% endfor %}
├── Rakefile ⇐ For tasks
├── README.md
├── references.md
├── resources.md
├── tema0-presentacion ⇐ Pages folders
│ ├── README.md
│ └── ...
├── tema ...
├── tfa
│ └── README.md
└── timetables.md
58 directories, 219 files
intro2sd
para señalar el punto de entrega de la anterior y se hace la entrega de esta tarea en la rama main
.