Featured image of post Amend Hugo Theme Even

Amend Hugo Theme Even

By default, the theme even doesn’t support the search and copy to the clipboard button which is proposed in issues#289 and issues#399. So I have finished implementing the features.

1. Search

Initially, you should add the output of JSON in your ./config.toml file.

1
2
[outputs]
  home = ["HTML", "RSS", "JSON"]

Then create the file ./themes/even/layouts/_default/index.json and add these lines. Then make sure you can see the output in the localhost:1313/index.json.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{- $.Scratch.Add "index" slice -}}
{{- range $index, $element := .Site.RegularPages.ByTitle -}}
  {{if ne .Params.tags nil}}{{if ne .Plain nil}}
  {{- $.Scratch.Add "index" (dict 
      "id" $index 
      "date" .Date 
      "tags" .Params.tags 
      "categories" .Params.categories 
      "title" .Title
      "permalink" .Permalink 
      "contents" .Plain ) -}}
  {{end}}{{end}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

And add file ./themes/even/static/js/search.js. I have stored the search object so just need to load the file one time once you enter the search page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const summaryInclude = 100;

let fuseOptions = {
  includeMatches: true,
  threshold: 0.1,
  tokenize: true,
  location: 0,
  distance: 100000,
  maxPatternLength: 32,
  minMatchCharLength: 1,
  keys: [
    {name: "title", weight: 0.9},
    {name: "contents", weight: 0.8},
    {name: "tags", weight: 0.3},
    {name: "categories", weight: 0.3}
  ]
};

let fuse; 
fetch("../index.json")
  .then(resp => resp.json())
  .then(data => fuse = new Fuse(data, fuseOptions));

let searchContent = "";

async function executeSearch() {
  searchContent = $('#search-content')[0].value;
  let results = fuse.search(searchContent);
  await populateResults(results);
}

function highlight(str) { 
  return str.replace(
    new RegExp(searchContent, 'gi'),
      searchContent => `<strong><span style="background-color:yellow;">${searchContent}</span></strong>`
    );
}

async function populateResults(result){
  $('#search-results').html('');
  $.each(result, function(key, value) {
    let contents = value.item.contents;
    let snippet = "", start = 0, end = 0;
    let pos = contents.toLocaleLowerCase().indexOf(searchContent.toLocaleLowerCase());
    if (pos != -1) {
      start = pos - summaryInclude;
      end = pos + summaryInclude;
      if (start < 0) start = 0;
      if (end > contents.length) end = contents.length;
      snippet = contents.substring(start, end);
    }

    $.each(value.matches,function(k, v) {
      start = v.indices[0][0] - summaryInclude;
      end = v.indices[0][1] + summaryInclude + 1;
      if (start < 0) start = 0;
      if (end > v.value.length) end = v.value.length;
      if (pos == -1 && v.key == "contents") {
        snippet += v.value.substring(start, end);
      }
    });

    if(snippet.length < 1){
      snippet += contents.substring(0, summaryInclude * 2);
    }

    let tags = [];
    value.item.tags.forEach(t => tags.push(highlight(t)));

    let categories = [];
    value.item.categories.forEach(c => categories.push(highlight(c)));

    let template = `
    <div id="summary-${key}">
      <article style="padding: 0px" class="post">
        <header class="post-header">
          <h1 class="post-title"><a style="color:black" class="post-link" href="${value.item.permalink}">${highlight(value.item.title)}</a></h1>
          <div class="post-meta">
            <span class="post-time">${value.item.date}</span>
            <div class="post-category"><a href="/categories/${value.item.categories}/"> ${categories} </a></div>
          </div>
        </header>
        
        <div class="post-content">
          <div class="post-summary">
            ${highlight(snippet)}
            <a href="${value.item.permalink}" class="read-more-link">Read more...</a>
          </div>
          <footer class="post-footer">
            <div class="post-tags">
              <a href="/tags/${value.item.tags}">${tags}</a>
            </div>
          </footer>
        </div>
      </article>
    </div>`;
    $('#search-results').append(template);
  });
}

And then just add the search page in your ./content/post/search.md. Although it’s not recommended to add the script tag in the middle of your markdown file, it is easy to implement the style that we expected.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
---
title: "Search"
layout: "search"
priority : 1
menu: "main"
weight: 5
---

<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2"></script>
<script src="/js/search.js"></script>
<section id="search-input">
  <input style="display: center" 
         id="search-content" 
         placeholder="Search text" 
         type="search" 
         oninput="executeSearch()"> 🔍 
</section>
<hr>
<section id="search-results">
  <h3>Please input some keywords to search</h3>
</section>

2. Copy to clipboard

I just add these lines to the end of ./themes/even/layouts/partials/scripts.html.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
function createCopyButton(highlightDiv) {
  const div = document.createElement("div");
  div.className = "copy-code";
  div.innerText = "Copy";
  div.addEventListener("click", () =>
    copyCodeToClipboard(div, highlightDiv)
  );
  addCopyButtonToDom(div, highlightDiv);
}

async function copyCodeToClipboard(button, highlightDiv) {
  const codeToCopy = highlightDiv.querySelector(":last-child > .chroma > code")
    .innerText;
  try {
    result = await navigator.permissions.query({ name: "clipboard-write" });
    if (result.state == "granted" || result.state == "prompt") {
      await navigator.clipboard.writeText(codeToCopy);
    } else {
      copyCodeBlockExecCommand(codeToCopy, highlightDiv);
    }
  } catch (_) {
    copyCodeBlockExecCommand(codeToCopy, highlightDiv);
  } finally {
    codeWasCopied(button);
  }
}

function codeWasCopied(div) {
  div.blur();
  div.innerText = "Copied!";
  setTimeout(function () {
    div.innerText = "Copy";
  }, 2000);
}

function addCopyButtonToDom(button, highlightDiv) {
  highlightDiv.insertBefore(button, highlightDiv.firstChild);
  const wrapper = document.createElement("div");
  wrapper.className = "highlight-wrapper";
  highlightDiv.parentNode.insertBefore(wrapper, highlightDiv);
  wrapper.appendChild(highlightDiv);
}

document.querySelectorAll(".highlight")
  .forEach((highlightDiv) => createCopyButton(highlightDiv));

</script>

And modify the ./themes/even/assets/sass/_partial/_post/_code.sass file, add these lines. I think it is a good idea to make the theme style the same as the type on the left of the code block.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.copy-code {
  position: absolute;
  right: 0;
  z-index: 2;
  font-size: .9em !important;
  padding: 0px 1.5rem !important;
  color: #b1b1b1;
  font-family: Arial;
  font-weight: bold;
  cursor: pointer;
  user-select: none;
}
I just want a peaceful life without troubles
Built with Hugo
Theme Stack designed by Jimmy