fido-node.github.io/posts/improve_code_blocks.html

458 lines
21 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><meta author="Alex Mikhailov"/><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/><meta name="color-scheme" content="light dark"/><meta http-equiv="content-language" content="en-us"/><meta name="description" content="Use highlight.js for code syntax highlighting"/><meta property="og:description" content="Use highlight.js for code syntax highlighting"/><meta property="og:image" content="https://fidonode.me/resources/images/preview/posts/improve_code_blocks.org.png"/><meta property="og:title" content="Improve code blocks"/><meta name="twitter:description" content="Use highlight.js for code syntax highlighting"/><meta name="twitter:title" content="Improve code blocks"/><meta name="twitter:image" content="https://fidonode.me/resources/images/preview/posts/improve_code_blocks.org.png"/><meta name="twitter:card" content="summary_large_image"/><link rel="icon" type="image/x-icon" href="/resources/favicon.ico"/><link rel="stylesheet" type="text/css" href="/resources/css/pico.sand.min.css"/><script defer="true" src="https://umami.dokutsu.xyz/script.js" data-website-id="d52d9af1-0c7d-4531-84c6-0b9c2850011f"></script><title>Improve code blocks</title><link id="highlight-theme" rel="stylesheet" type="text/css"/><script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script><script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js"></script><script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/lisp.min.js"></script><script src="/resources/js/theme-selector.js"></script></head><body><header class="header"><div class="container"><nav><ul><li><strong>Alex Mikhailov</strong></li></ul><ul><li><a href="/index.html">About</a></li><li><a href="/posts.html">Blog</a></li><li><a href="/rss.xml">RSS</a></li></ul></nav></div></header><main class="container blog-post"><hgroup><h1>Improve code blocks</h1><p>Use highlight.js for code syntax highlighting</p><nav><ul><li>Tags:</li><li><mark><a href="/tags/@org-mode.html" class="secondary">@org-mode</a></mark></li><li><mark><a href="/tags/@elisp.html" class="secondary">@elisp</a></mark></li><li><mark><a href="/tags/@highlightjs.html" class="secondary">@highlightjs</a></mark></li></ul></nav></hgroup><div id="table-of-contents" role="doc-toc">
<h2>Table of Contents</h2>
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="#orgd0a425a">What is the problem with default highlighting?</a></li>
<li><a href="#org712f60e">Highlight.js</a>
<ul>
<li><a href="#orgcd43fbb">Change code block template</a></li>
<li><a href="#orgb412f22">Plug Highlight.js</a></li>
<li><a href="#org5179bb6">Respect prefers-color-scheme</a></li>
</ul>
</li>
<li><a href="#orgedc7142">Whole config</a></li>
</ul>
</div>
</div>
<div id="outline-container-orgd0a425a" class="outline-2">
<h2 id="orgd0a425a">What is the problem with default highlighting?</h2>
<div class="outline-text-2" id="text-orgd0a425a">
<p>
Htmlize works poorly with headless publishing. It lacks extensibility, including features like line numbers, a copy button, and the ability to highlight predefined parts of the code.
</p>
</div>
</div>
<div id="outline-container-org712f60e" class="outline-2">
<h2 id="org712f60e">Highlight.js</h2>
<div class="outline-text-2" id="text-org712f60e">
</div>
<div id="outline-container-orgcd43fbb" class="outline-3">
<h3 id="orgcd43fbb">Change code block template</h3>
<div class="outline-text-3" id="text-orgcd43fbb">
<p>
We need to make small changes in how code blocks are rendered. By default, Org Export exports code blocks as <code>&lt;pre&gt;&lt;/pre&gt;</code>. For Highlight.js, we need <code>&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;</code>. Additionally, we need to set the correct language name in the class attribute. Since Highlight.js does not support <code>elisp</code>, I rewrite it to regular <code>lisp</code> using the <code>my/replace-substring</code> function.
</p>
<pre><code class="language-lisp">(setq
my/lang-substitution-map &apos;((&quot;elisp&quot; . &quot;lisp&quot;)))
(defun my/replace-substrings (input-string)
&quot;Replace substrings in INPUT-STRING according to SUBSTITUTION-MAP.&quot;
(let ((output-string input-string))
(dolist (pair my/lang-substitution-map)
(let ((old (regexp-quote (car pair)))
(new (cdr pair)))
(setq output-string (replace-regexp-in-string old new output-string))))
output-string))
(defun my/src-block (src-block contents info)
&quot;Translate SRC-BLOCK element into HTML.
CONTENTS is nil. INFO is a plist holding contextual information.&quot;
(let* (
(org-language (format &quot;language-%s&quot; (org-element-property :language src-block)))
(language (my/replace-substrings org-language))
(code (org-element-property :value src-block)))
(esxml-to-xml
`(pre ()
(code ((class . ,language))
,(org-html-encode-plain-text code)
))
)
)
)
(org-export-define-derived-backend &apos;my-html &apos;html
:translate-alist &apos;(
(template . my/template)
(src-block . my/src-block)
))
</code></pre>
</div>
</div>
<div id="outline-container-orgb412f22" class="outline-3">
<h3 id="orgb412f22">Plug Highlight.js</h3>
<div class="outline-text-3" id="text-orgb412f22">
<p>
I do not want to load Highlight.js on every page, so I need to check if the initial Org file contains code blocks. Depending on this, we will render the part of the tree with JavaScript or not.
</p>
<pre><code class="language-lisp">(defun my/org-has-src-blocks-p (info)
&quot;Return t if the Org document represented by INFO has source code blocks.&quot;
(org-element-map (plist-get info :parse-tree) &apos;src-block
(lambda (src-block) t)
nil t))
(defun my/template (contents info)
(let* ((title-str (org-export-data (plist-get info :title) info))
...
(has-src-blocks (my/org-has-src-blocks-p info)))
...
(script ((defer . &quot;true&quot;) (src . &quot;https://umami.dokutsu.xyz/script.js&quot;) (data-website-id . &quot;d52d9af1-0c7d-4531-84c6-0b9c2850011f&quot;)) ())
,(when has-src-blocks
`(nil ()
(link ((id . &quot;highlight-theme&quot;) (rel . &quot;stylesheet&quot;) (type . &quot;text/css&quot;)))
(script ((src . &quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js&quot;)) ())
(script ((src . &quot;https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js&quot;)) ())
(script ((src . &quot;https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/lisp.min.js&quot;)) ())
(script ((src . &quot;/resources/js/theme-selector.js&quot;)) ())
)
)
(title () ,title-str)
</code></pre>
</div>
</div>
<div id="outline-container-org5179bb6" class="outline-3">
<h3 id="org5179bb6">Respect prefers-color-scheme</h3>
<div class="outline-text-3" id="text-org5179bb6">
<p>
Additionally, I think it's a good idea to respect the <code>prefers-color-scheme</code> property of the user's browser. We can switch CSS files based on this property. We should also subscribe to changes in this property to handle the edge case when it switches while reading the page.
</p>
<pre><code class="language-javascript">hljs.highlightAll(); // Initialize highlight.js
// Function to set the theme based on the preferred color scheme
function setHighlightTheme() {
const highlightThemeLink = document.getElementById(&quot;highlight-theme&quot;);
const darkTheme =
&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/base16/solarized-dark.min.css&quot;;
const lightTheme =
&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/base16/solarized-light.min.css&quot;;
if (
window.matchMedia &amp;amp;&amp;amp;
window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches
) {
highlightThemeLink.href = darkTheme;
} else {
highlightThemeLink.href = lightTheme;
}
}
// Initial theme set
setHighlightTheme();
// Listen for changes in the preferred color scheme
window
.matchMedia(&quot;(prefers-color-scheme: dark)&quot;)
.addEventListener(&quot;change&quot;, setHighlightTheme);
</code></pre>
</div>
</div>
</div>
<div id="outline-container-orgedc7142" class="outline-2">
<h2 id="orgedc7142">Whole config</h2>
<div class="outline-text-2" id="text-orgedc7142">
<p>
In between posts I've switched from <code>sxml</code> to <code>esxml</code> so here is the current config.
</p>
<pre><code class="language-lisp">;; Load the publishing system
;; Configure environment
;;
(setq debug-on-error t)
(let ((default-directory (concat &quot;~/.config/emacs/.local/straight/build-&quot; emacs-version &quot;/&quot;)))
(normal-top-level-add-subdirs-to-load-path))
(add-to-list &apos;custom-theme-load-path
(concat &quot;~/.config/emacs/.local/straight/build-&quot; emacs-version &quot;/doom-themes&quot;))
(add-to-list &apos;custom-theme-load-path (concat &quot;~/.config/emacs/.local/straight/build-&quot; emacs-version &quot;/base16-theme&quot;))
(add-to-list &apos;custom-theme-load-path (concat &quot;~/.config/emacs/.local/straight/build-&quot; emacs-version &quot;/moe-theme&quot;))
(require &apos;xml)
(require &apos;dom)
(require &apos;ox-publish)
(require &apos;ox-rss)
(require &apos;org)
(require &apos;esxml)
;; (require &apos;esxml-html)
;;
;;Variables
;;
(setq
my/url &quot;https://fidonode.me&quot;
my/web-export-path &quot;./public&quot;
my/blog-src-path &quot;./home/05 Blog&quot;
my/lang-substitution-map &apos;((&quot;elisp&quot; . &quot;lisp&quot;))
org-html-validation-link nil ;; Don&apos;t show validation link
org-html-htmlize-output-type nil
org-src-fontify-natively t)
;;
;;Templates
;;
(defun my/footer (info)
`(footer ((class . &quot;footer&quot;))
(hr () )
(small ()
(p () &quot;Alex Mikhailov&quot;)
(p () &quot;Built with: &quot;
(a ((href . &quot;https://www.gnu.org/software/emacs/&quot;)) &quot;GNU Emacs&quot;) &quot; &quot;
(a ((href . &quot;https://orgmode.org/&quot;)) &quot;Org Mode&quot;) &quot; &quot;
(a ((href . &quot;https://picocss.com/&quot;)) &quot;picocss&quot;)
)
)
))
(defun my/header (info)
(let ((title-str (org-export-data (plist-get info :title) info)))
`(header ((class . &quot;header&quot;))
(nav ()
(ul ()
(li ()
(strong () ,title-str)))
(ul ()
(li () (a ((href . &quot;/index.html&quot;)) &quot;About&quot;))
(li () (a ((href . &quot;/posts.html&quot;)) &quot;Blog&quot;))
(li () (a ((href . &quot;/rss.xml&quot;)) &quot;RSS&quot;))
)
))
)
)
(defun my/src-block (src-block contents info)
&quot;Translate SRC-BLOCK element into HTML.
CONTENTS is nil. INFO is a plist holding contextual information.&quot;
(let* (
(org-language (format &quot;language-%s&quot; (org-element-property :language src-block)))
(language (my/replace-substrings org-language))
(code (org-element-property :value src-block)))
(esxml-to-xml
`(pre ()
(code ((class . ,language))
,(org-html-encode-plain-text code)
))
)
)
)
(defun my/template (contents info)
(let* ((title-str (org-export-data (plist-get info :title) info))
(description-str (org-export-data (plist-get info :description) info))
(file-path-str (org-export-data (plist-get info :input-file) info))
(base-directory-str (org-export-data (plist-get info :base-directory) info))
(file-name-str (file-relative-name file-path-str (format &quot;%s/%s&quot; script-directory base-directory-str)))
(img-link-str (format &quot;%s/resources/images/%s.png&quot; my/url file-name-str))
(has-src-blocks (my/org-has-src-blocks-p info)))
(set-text-properties 0 (length title-str) nil title-str)
(set-text-properties 0 (length description-str) nil description-str)
(set-text-properties 0 (length img-link-str) nil img-link-str)
(concat
&quot;&amp;lt;!DOCTYPE html&amp;gt;&quot;
(esxml-to-xml
`(html ((lang . &quot;en&quot;))
(head ()
(meta ((charset . &quot;utf-8&quot;)))
(meta ((author . &quot;Alex Mikhailov&quot;)))
(meta ((name . &quot;viewport&quot;)
(content . &quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;)))
(meta ((name . &quot;color-scheme&quot;) (content . &quot;light dark&quot;)))
(meta ((http-equiv . &quot;content-language&quot;) (content . &quot;en-us&quot;)))
;; OG block
;; &quot;Personal page with a blog about my technical adventures&quot;
(meta ((name . &quot;description&quot;) (content . ,description-str)))
(meta ((name . &quot;og:description&quot;) (content . ,description-str)))
(meta ((name . &quot;twitter:description&quot;) (content . ,description-str)))
(meta ((name . &quot;og:image&quot;) (content . ,img-link-str)))
(meta ((name . &quot;twitter:image&quot;) (content . ,img-link-str)))
(meta ((name . &quot;og:title&quot;) (content . ,title-str)))
(meta ((name . &quot;twitter:title&quot;) (content . ,title-str)))
(meta ((name . &quot;twitter:card&quot;) (content . &quot;summary_large_image&quot;)))
(link ((rel . &quot;icon&quot;) (type . &quot;image/x-icon&quot;) (href . &quot;/resources/favicon.ico&quot;)))
(link ((rel . &quot;stylesheet&quot;) (type . &quot;text/css&quot;) (href . &quot;/resources/css/pico.sand.min.css&quot;)))
(script ((defer . &quot;true&quot;) (src . &quot;https://umami.dokutsu.xyz/script.js&quot;) (data-website-id . &quot;d52d9af1-0c7d-4531-84c6-0b9c2850011f&quot;)) ())
,(when has-src-blocks
`(nil ()
(link ((id . &quot;highlight-theme&quot;) (rel . &quot;stylesheet&quot;) (type . &quot;text/css&quot;)))
(script ((src . &quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js&quot;)) ())
(script ((src . &quot;https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js&quot;)) ())
(script ((src . &quot;https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/lisp.min.js&quot;)) ())
(script ((src . &quot;/resources/js/theme-selector.js&quot;)) ())
)
)
(title () ,title-str)
)
(body ()
(main ((class . &quot;container&quot;))
,(my/header info)
(raw-string ,contents)
,(my/footer info)
)
))
))
))
(org-export-define-derived-backend &apos;my-html &apos;html
:translate-alist &apos;(
(template . my/template)
(src-block . my/src-block)
))
(defun my/publish-to-html (plist filename pub-dir)
&quot;Publish an Org file to HTML using the custom backend.&quot;
(org-publish-org-to &apos;my-html filename &quot;.html&quot; plist pub-dir))
;;
;;Sitemap/RSS
;;
(defun my/format-rss-feed-entry (entry style project)
&quot;Format ENTRY for the RSS feed.
ENTRY is a file name. STYLE is either &apos;list&apos; or &apos;tree&apos;.
PROJECT is the current project.&quot;
(cond ((not (directory-name-p entry))
(let* ((file (org-publish--expand-file-name entry project))
(title (org-publish-find-title entry project))
(date (format-time-string &quot;%Y-%m-%d&quot; (org-publish-find-date entry project)))
(link (concat (file-name-sans-extension entry) &quot;.html&quot;)))
(with-temp-buffer
(org-mode)
(insert (format &quot;* [[file:%s][%s]]\n&quot; file title))
(org-set-property &quot;RSS_PERMALINK&quot; link)
(org-set-property &quot;RSS_TITLE&quot; title)
(org-set-property &quot;PUBDATE&quot; date)
(let ((first-two-lines (with-temp-buffer
(insert-file-contents file)
(buffer-substring-no-properties
(point-min)
(progn (forward-line 2) (point))))))
(if (string-suffix-p &quot;\n&quot; first-two-lines)
(setq first-two-lines (substring first-two-lines 0 -1)))
(insert first-two-lines))
(goto-char (point-max))
(insert &quot;...&quot;)
(buffer-string))))
((eq style &apos;tree)
;; Return only last subdir.
(file-name-nondirectory (directory-file-name entry)))
(t entry)))
(defun my/format-rss-feed (title list)
&quot;Generate RSS feed, as a string.
TITLE is the title of the RSS feed. LIST is an internal
representation for the files to include, as returned by
`org-list-to-lisp&apos;. PROJECT is the current project.&quot;
(concat &quot;#+TITLE: &quot; title &quot;\n&quot;
&quot;#+STARTUP: showall \n\n&quot;
(org-list-to-subtree list 1 &apos;(:icount &quot;&quot; :istart &quot;&quot;))))
(defun my/publish-to-rss (plist filename pub-dir)
&quot;Publish RSS with PLIST, only when FILENAME is &apos;rss.org&apos;.
PUB-DIR is when the output will be placed.&quot;
(if (equal &quot;rss.org&quot; (file-name-nondirectory filename))
(org-rss-publish-to-rss plist filename pub-dir)))
;;
;;Helpers
;;
(defun my/format-date-subtitle (file project)
&quot;Format the date found in FILE of PROJECT.&quot;
(format-time-string &quot;posted on %Y-%m-%d&quot; (org-publish-find-date file project)))
(defun my/pt (var)
&quot;Print the value and type of VAR.&quot;
(message &quot;Value: %S, Type: %s&quot; var (type-of var)))
(defun plist-keys (plist)
&quot;Return a list of keys in the property list PLIST.&quot;
(let (keys)
(while plist
(setq keys (cons (car plist) keys))
(setq plist (cddr plist)))
(nreverse keys)))
(defvar script-directory
(file-name-directory (or load-file-name buffer-file-name))
&quot;The directory where the current script is located.&quot;)
(defun my/org-has-src-blocks-p (info)
&quot;Return t if the Org document represented by INFO has source code blocks.&quot;
(org-element-map (plist-get info :parse-tree) &apos;src-block
(lambda (src-block) t)
nil t))
(defun my/replace-substrings (input-string)
&quot;Replace substrings in INPUT-STRING according to SUBSTITUTION-MAP.&quot;
(let ((output-string input-string))
(dolist (pair my/lang-substitution-map)
(let ((old (regexp-quote (car pair)))
(new (cdr pair)))
(setq output-string (replace-regexp-in-string old new output-string))))
output-string))
;;
;;Clear folder with results
;;
(when (file-directory-p my/web-export-path)
(delete-directory my/web-export-path t))
(mkdir my/web-export-path)
;;
;;Main blog configuration
;;
(setq org-publish-project-alist
(list
(list &quot;static&quot;
:base-directory my/blog-src-path
:base-extension &quot;css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|pdf\\|ico\\|txt&quot;
:publishing-directory my/web-export-path
:recursive t
:publishing-function &apos;org-publish-attachment
)
(list &quot;blog&quot;
:recursive t
:base-directory my/blog-src-path
:publishing-directory my/web-export-path
:publishing-function &apos;my/publish-to-html
:html-html5-fancy t
:htmlized-source t
:with-author nil
:with-creator t
:with-toc t
:section-numbers nil
:time-stamp-file nil
)
(list &quot;blog-rss&quot;
:author &quot;Alex M&quot;
:email &quot;iam@fidonode.me&quot;
:base-directory my/blog-src-path
:base-extension &quot;org&quot;
:recursive t
:exclude (regexp-opt &apos;(&quot;rss.org&quot; &quot;index.org&quot; &quot;404.org&quot; &quot;posts.org&quot;))
:publishing-function &apos;my/publish-to-rss
:publishing-directory my/web-export-path
:rss-extension &quot;xml&quot;
:html-link-home my/url
:html-link-use-abs-url t
:html-link-org-files-as-html t
:auto-sitemap t
:sitemap-filename &quot;rss.org&quot;
:sitemap-title &quot;rss&quot;
:sitemap-style &apos;list
:sitemap-sort-files &apos;anti-chronologically
:sitemap-function &apos;my/format-rss-feed
:sitemap-format-entry &apos;my/format-rss-feed-entry)
))
;; Generate the site output
(org-publish-all t)
(message &quot;Build complete!&quot;)
</code></pre>
</div>
</div>
</main><footer class="footer"><div class="container"><hr/><small><p>Alex Mikhailov</p><p>Built with: <a href="https://www.gnu.org/software/emacs/">GNU Emacs</a> <a href="https://orgmode.org/">Org Mode</a> <a href="https://picocss.com/">picocss</a></p></small></div></footer></body></html>