<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
	<title>Sacha Chua - category - js</title>
	<atom:link href="https://sachachua.com/blog/category/js/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/js" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/js/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Mon, 20 Apr 2026 13:21:38 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>Org Mode: JS for translating times to people's local timezones</title>
		<link>https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 14 Apr 2026 18:44:16 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
<category>js</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/</guid>
		<description><![CDATA[<p>
I want to get back into the swing of doing <a href="https://sachachua.com/topic/emacs-chat/">Emacs Chats</a> again, which means scheduling, which means timezones. Let's see first if anyone happens to match up with the Thursday timeslots (10:30 or 12:45) that I'd like to use for Emacs-y video things, but I might be able to shuffle things around if needed.
</p>

<p>
I want something that can translate times into people's local timezones.
I use Org Mode timestamps a lot because they're so easy to insert with <code>C-u C-c !</code> (<code>org-timestamp-inactive</code>), which inserts a timestamp like this:
</p>

<p>
<span class="timestamp-wrapper"><time class="timestamp" datetime="2026-04-16T10:30:00-0400">[2026-04-16 Thu 10:30]</time></span>
</p>

<p>
By default, the Org HTML export for it does not include the timezone offset. That's easily fixed by adding <code>%z</code> to the time specifier, like this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">setq</span> org-html-datetime-formats <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"%F"</span> . <span class="org-string">"%FT%T%z"</span>))
</code></pre>
</div>


<p>
Now a little bit of Javascript code makes it clickable and lets us toggle a translated time. I put the time afterwards so that people can verify it visually. I never quite trust myself when it comes to timezone translations.
</p>


<div class="org-src-container">
<pre class="src src-js"><code><span class="org-keyword">function</span> <span class="org-function-name">translateTime</span>(<span class="org-variable-name">event</span>) {
  <span class="org-keyword">if</span> (event.target.getAttribute(<span class="org-string">'datetime'</span>)?.match(<span class="org-string">/[0-9][0-9][0-9][0-9]$/</span>)) {
    <span class="org-keyword">if</span> (event.target.querySelector(<span class="org-string">'.translated'</span>)) {
      event.target.querySelectorAll(<span class="org-string">'.translated'</span>).forEach((o) =&gt; o.remove());
    } <span class="org-keyword">else</span> {
      <span class="org-keyword">const</span> <span class="org-variable-name">span</span> = document.createElement(<span class="org-string">'span'</span>);
      span.classList.add(<span class="org-string">'translated'</span>);
      span.textContent = <span class="org-string">' &#8594; '</span> + (<span class="org-keyword">new</span> <span class="org-type">Date</span>(event.target.getAttribute(<span class="org-string">'datetime'</span>))).toLocaleString(<span class="org-constant">undefined</span>, {
        month: <span class="org-string">'short'</span>,  
        day: <span class="org-string">'numeric'</span>,  
        hour: <span class="org-string">'numeric'</span>, 
        minute: <span class="org-string">'2-digit'</span>,
        timeZoneName: <span class="org-string">'short'</span>
      });
      event.target.appendChild(span);
    }
  }
}
<span class="org-keyword">function</span> <span class="org-function-name">clickForLocalTime</span>() {
  document.querySelectorAll(<span class="org-string">'time'</span>).forEach((o) =&gt; {
    <span class="org-keyword">if</span> (o.getAttribute(<span class="org-string">'datetime'</span>)?.match(<span class="org-string">/[0-9][0-9][0-9][0-9]$/</span>)) {
      o.addEventListener(<span class="org-string">'click'</span>, translateTime);
      o.classList.add(<span class="org-string">'clickable'</span>);
    }
  });
}
</code></pre>
</div>


<p>
And some CSS to make it more obvious that it's now clickable:
</p>


<div class="org-src-container">
<pre class="src src-css"><code><span class="org-css-selector">.clickable</span> {
    <span class="org-css-property">cursor</span>: pointer;
    <span class="org-css-property">text-decoration</span>: underline dotted;
}
</code></pre>
</div>


<p>
Let's see if this is useful.
</p>

<p>
Someday, it would probably be handy to have a button that translates all the timestamps in a table, but this is a good starting point.
</p>
<div><a href="https://sachachua.com/blog/2026/04/org-mode-js-for-translating-times-to-people-s-local-timezones/index.org">View Org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Forg-mode-js-for-translating-times-to-people-s-local-timezones%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Emacs Lisp and NodeJS: Getting the bolded words from a section of a Google Document</title>
		<link>https://sachachua.com/blog/2026/03/emacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 10 Mar 2026 18:21:24 GMT</pubDate>
    <category>french</category>
<category>js</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/03/emacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document/</guid>
		<description><![CDATA[<div class="update" id="org4a3fba1">
<ul class="org-ul">
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-03-13">[2026-03-13 Fri]</time></span>: Cleaned up links from Google</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2026-03-12">[2026-03-12 Thu]</time></span>: Simplified getting a section or finding the bolded text by using the Org Mode format instead.</li>
</ul>

</div>

<p>
During the sessions with my French tutor, I share a Google document so that we can mark the words where I need to practice my pronunciation some more or tweak the wording. Using Ctrl+B to make the word as bold is an easy way to make it jump out.
</p>

<p>
I used to copy these changes into my Org Mode notes manually, but today I thought I'd try automating some of it.
</p>

<p>
First, I need a script to download the HTML for a specified Google document. This is probably easier to do with the NodeJS library rather than with oauth2.el and <code>url-retrieve-synchronously</code> because of various authentication things.
</p>


<div class="org-src-container">
<pre class="src src-js"><code>require(<span class="org-string">'dotenv'</span>).config();
<span class="org-keyword">const</span> { google } = require(<span class="org-string">'googleapis'</span>);

<span class="org-keyword">async</span> <span class="org-keyword">function</span> download(<span class="org-variable-name">fileId</span>) {
  <span class="org-keyword">const</span> <span class="org-variable-name">auth</span> = <span class="org-keyword">new</span> <span class="org-type">google.auth.GoogleAuth</span>({
    scopes: [<span class="org-string">'https://www.googleapis.com/auth/drive.readonly'</span>],
  });
  <span class="org-keyword">const</span> <span class="org-variable-name">drive</span> = google.drive({ version: <span class="org-string">'v3'</span>, auth });
  <span class="org-keyword">const</span> <span class="org-variable-name">htmlRes</span> = <span class="org-keyword">await</span> drive.files.<span class="org-keyword">export</span>({
    fileId: fileId,
    mimeType: <span class="org-string">'text/html'</span>
  });
  <span class="org-keyword">return</span> htmlRes.data;
}

<span class="org-keyword">async</span> <span class="org-keyword">function</span> main() {
  console.log(<span class="org-keyword">await</span> download(process.argv.length &gt; 2 ? process.argv[2] : process.env[<span class="org-string">'DOC_ID'</span>]));
}

main();
</code></pre>
</div>


<p>
Then I can wrap a little bit of Emacs Lisp around it.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-google-doc-download-command</span>
  (list <span class="org-string">"nodejs"</span> (expand-file-name <span class="org-string">"~/bin/download-google-doc-html.cjs"</span>)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-google-doc-html</span> (doc-id)
  (<span class="org-keyword">when</span> (string-match <span class="org-string">"https://docs\\.google\\.com/document/d/</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">/"</span> doc-id)
    (<span class="org-keyword">setq</span> doc-id (match-string 1 doc-id)))
  (<span class="org-keyword">with-temp-buffer</span>
    (apply <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">call-process</span> (car my-google-doc-download-command)
           nil t nil (append (cdr my-google-doc-download-command) (list doc-id)))
    (buffer-string)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-google-doc-clean-html</span> (html)
  <span class="org-doc">"Remove links on spaces, replace Google links."</span>
  (<span class="org-keyword">let</span> ((dom (<span class="org-keyword">with-temp-buffer</span>
               (insert html)
               (libxml-parse-html-region))))
    (dom-search
     dom
     (<span class="org-keyword">lambda</span> (o)
       (<span class="org-keyword">when</span> (eq (dom-tag o) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">a</span>)
         (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">href</span>)
                    (string-match <span class="org-string">"https://</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">www\\.</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">?google\\.com/url\\?q="</span> (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">href</span>)))
           (<span class="org-keyword">let*</span> ((parsed (url-path-and-query
                           (url-generic-parse-url (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">href</span>))))
                  (params (url-parse-query-string (cdr parsed))))
             (dom-set-attribute o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">href</span> (car (assoc-default <span class="org-string">"q"</span> params <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>)))))
         (<span class="org-keyword">let</span> ((text (string= (string-trim (dom-text o)) <span class="org-string">""</span>)))
           (<span class="org-keyword">when</span> (string= text <span class="org-string">""</span>)
             (<span class="org-keyword">setf</span> (car o) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">span</span>))))
       (<span class="org-keyword">when</span> (<span class="org-keyword">and</span>
              (string-match <span class="org-string">"font-weight:700"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))
              (not (string-match <span class="org-string">"font-style:normal"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))))
         (<span class="org-keyword">setf</span> (car o) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">strong</span>))
       (<span class="org-keyword">when</span> (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
         (dom-remove-attribute o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">bold text is actually represented as font-weight:700 instead</span>
    (<span class="org-keyword">with-temp-buffer</span>
      (svg-print dom)
      (buffer-string))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-google-doc-org</span> (doc-id)
  <span class="org-doc">"Return DOC-ID in Org Mode format."</span>
  (pandoc-convert-stdio (my-google-doc-clean-html (my-google-doc-html doc-id)) <span class="org-string">"html"</span> <span class="org-string">"org"</span>))
</code></pre>
</div>


<p>
I have lots of sections in that document, including past journal entries, so I want to get a specific section by name.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-get-subtree-by-name</span> (org-text heading-name)
  <span class="org-doc">"Return ORG-TEXT subtree for HEADING-NAME."</span>
  (<span class="org-keyword">with-temp-buffer</span>
    (insert org-text)
    (org-mode)
    (goto-char (point-min))
    (<span class="org-keyword">let</span> ((org-trust-scanner-tags t))
      (car (delq nil
                 (org-map-entries
                  (<span class="org-keyword">lambda</span> ()
                    (<span class="org-keyword">when</span> (string= (org-entry-get (point) <span class="org-string">"ITEM"</span>) heading-name)
                      (buffer-substring (point) (org-end-of-subtree))))))))))
</code></pre>
</div>


<p>
Now I can get the bolded words from a section of my notes, with just a sentence for context. I use pandoc to convert it to Org Mode syntax.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-lang-words-for-review-context-function</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">sentence-at-point</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-lang-tutor-notes-url</span> nil)
(<span class="org-keyword">defun</span> <span class="org-function-name">my-lang-tutor-notes</span> (section-name)
  (my-org-get-subtree-by-name
   (my-google-doc-org my-lang-tutor-notes-url)
   section-name))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-lang-words-for-review</span> (section)
  <span class="org-doc">"List the bolded words for review in SECTION."</span>
  (<span class="org-keyword">let*</span> ((section (my-lang-tutor-notes section))
         results)
    (<span class="org-keyword">with-temp-buffer</span>
      (insert section)
      (org-mode)
      (goto-char (point-min))
      (org-map-entries
       (<span class="org-keyword">lambda</span> ()
         (org-end-of-meta-data t)
         (<span class="org-keyword">while</span> (re-search-forward <span class="org-string">"\\*[</span><span class="org-string"><span class="org-negation-char">^</span></span><span class="org-string">* ].*?\\*"</span> nil t)
           (<span class="org-keyword">cl-pushnew</span>
            (replace-regexp-in-string
             <span class="org-string">"[&#160;\n ]+"</span> <span class="org-string">" "</span>
             (funcall my-lang-words-for-review-context-function))
            results
            <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">string=</span>)))))
    (nreverse results)))
</code></pre>
</div>


<p>
For example, when I run it on my <a href="https://sachachua.com/blog/2026/03/la-semaine-du-2-mars-au-8-mars/#entr-es-de-journal-la-semaine-du-2-mars-au-8-mars-dimanche-8-mars-sur-l-intelligence-artificielle">notes on artificial intelligence</a>, this is the list of bolded words and the sentences that contain them.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(my-lang-words-for-review <span class="org-string">"Sur l'intelligence artificielle"</span>)
</code></pre>
</div>


<ul class="org-ul">
<li>Je l'ai aussi utilisée pour faire des <b>recherches</b>.</li>
<li>Je peux consacrer une petite partie de mon <b>budget</b> à des essais, mais je ne veux pas travailler davantage pour rentabiliser une dépense plus importante.</li>
<li>Je n'ai pas le temps de concentration nécessaire pour justifier l'investissement dans mon propre matériel, et <b>sinon</b>, les progrès sont trop rapides pour m'engager dans une configuration spécifique.</li>
<li>J'ai une conscience <b>aiguë</b> des limites cognitives ou physiques à cause des difficultés <b>de santé</b> de ma mère et de ma sœur, et de mes expériences avec mes limitations à cause du fait que je suis la personne principalement en charge de ma fille.</li>
<li>Je lis très vite, mais je n'ai pas assez de patience pour les longs <b>contenus</b> vidéo ou audio.</li>
<li>Je n'aime pas les textes qui <b>contiennent</b> beaucoup de remplissage.</li>
<li>Beaucoup de gens ont une réaction forte contre l'IA pour plusieurs raisons qui <b>incluent</b> le battage médiatique excessif dont elle fait l'objet, son utilisation à mauvais escient, et <b>l'inondation de banalité</b> qu'elle produit.</li>
<li><b>Je</b> réécris souvent la majorité du logiciel à l'exception d'un ou deux morceaux parce que ce code ne me convient pas.</li>
<li>Je ne veux pas l'utiliser pour les <b>correctifs</b> que je veux soumettre à d'autres projets parce que le <b>code</b> ne me semble pas correct et je ne veux pas <b>gaspiller</b> le temps d'autres bénévoles.</li>
<li>J'aime pouvoir lui donner trois dépôts git et des instructions pour générer un logiciel à partir d'un dépôt pour <b>un autre</b> via le troisième dépôt.</li>
<li>Mais je ne veux pas le <b>publier</b> avant de réécrire et tout comprendre.</li>
<li>Sans l'IA, je pourrais peut-être apprendre plus lentement avec l'aide <b>d'Internet</b>, qui a beaucoup de ressources comme<a href="https://vitrinelinguistique.oqlf.gouv.qc.ca/">https://vitrinelinguistique.oqlf.gouv.qc.ca/</a><a href="https://vitrinelinguistique.oqlf.gouv.qc.ca/">Vitrine linguistique</a>.</li>
<li>Je veux profiter davantage, apprendre davantage avec <b>l'aide</b> de vraies personnes, complétée par l'aide de l'IA.</li>
<li>J'adore les sous-titres simultanés, mais je n'ai pas toujours trouvé une méthode ou un système qui me <b>convienne</b>.</li>
</ul>

<p>
I can then go into the WhisperX transcription JSON file and replay those parts for closer review.
</p>

<p>
I can also tweak the context function to give me less information. For example, to limit it to the containing phrase, I can do this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-split-string-keep-delimiters</span> (string delimiter)
  (<span class="org-keyword">when</span> string
    (<span class="org-keyword">let</span> (results pos)
      (<span class="org-keyword">with-temp-buffer</span>
        (insert string)
        (goto-char (point-min))
        (<span class="org-keyword">setq</span> pos (point-min))
        (<span class="org-keyword">while</span> (re-search-forward delimiter nil t)
          (<span class="org-keyword">push</span> (buffer-substring pos (match-beginning 0)) results)
          (<span class="org-keyword">setq</span> pos (match-beginning 0)))
        (<span class="org-keyword">push</span> (buffer-substring pos (point-max)) results)
        (nreverse results)))))

(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">my-split-string-keep-delimiters</span> ()
 (<span class="org-keyword">should</span>
  (equal (my-split-string-keep-delimiters
          <span class="org-string">"Beaucoup de gens ont une r&#233;action forte contre l'IA pour plusieurs raisons qui *incluent*&#160;le battage m&#233;diatique excessif dont elle fait l'objet, son utilisation &#224; mauvais escient, et *l'inondation de banalit&#233;*&#160;qu'elle produit."</span>
          <span class="org-string">", </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> que </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qui </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'ils? </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'elles? </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'on "</span>
          )
 )))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-lang-words-for-review-phrase-context</span> (<span class="org-type">&amp;optional</span> s)
  (<span class="org-keyword">setq</span> s (replace-regexp-in-string <span class="org-string">"&#160;"</span> <span class="org-string">" "</span> (<span class="org-keyword">or</span> s (sentence-at-point))))
  (string-join
   (seq-filter (<span class="org-keyword">lambda</span> (s) (string-match <span class="org-string">"\\*"</span> s))
               (my-split-string-keep-delimiters s <span class="org-string">", </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> parce que </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> que </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qui </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'ils? </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'elles? </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> qu'on </span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string"> pour "</span>))
   <span class="org-string">" ... "</span>))

(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">my-lang-words-for-review-phrase-context</span> ()
  (<span class="org-keyword">should</span>
   (equal (my-lang-words-for-review-phrase-context
           <span class="org-string">"Je peux consacrer une petite partie de mon *budget*&#160;&#224; des essais, mais je ne veux pas travailler davantage pour rentabiliser une d&#233;pense plus importante."</span>)
          <span class="org-string">"Je peux consacrer une petite partie de mon *budget*&#160;&#224; des essais"</span>)))
</code></pre>
</div>



<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">let</span> ((my-lang-words-for-review-context-function <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-lang-words-for-review-phrase-context</span>))
  (my-lang-words-for-review <span class="org-string">"Sur l'intelligence artificielle"</span>))
</code></pre>
</div>


<ul class="org-ul">
<li>pour faire des <b>recherches</b>.</li>
<li>Je peux consacrer une petite partie de mon <b>budget</b> à des essais</li>
<li>, et <b>sinon</b></li>
<li>J'ai une conscience <b>aiguë</b> des limites cognitives ou physiques à cause des difficultés <b>de santé</b> de ma mère et de ma sœur</li>
<li>pour les longs <b>contenus</b> vidéo ou audio.</li>
<li>Je n'aime pas les textes qui <b>contiennent</b> beaucoup de remplissage.</li>
<li>qui <b>incluent</b> le battage médiatique excessif dont elle fait l'objet &hellip; , et <b>l'inondation de banalité</b></li>
<li><b>Je</b> réécris souvent la majorité du logiciel à l'exception d'un ou deux morceaux</li>
<li>pour les <b>correctifs</b> &hellip; parce que le <b>code</b> ne me semble pas correct et je ne veux pas <b>gaspiller</b> le temps d'autres bénévoles.</li>
<li>pour <b>un autre</b> via le troisième dépôt.</li>
<li>Mais je ne veux pas le <b>publier</b> avant de réécrire et tout comprendre.</li>
<li>, je pourrais peut-être apprendre plus lentement avec l'aide <b>d'Internet</b></li>
<li>, apprendre davantage avec <b>l'aide</b> de vraies personnes, complétée par l'aide de l'IA.</li>
<li>qui me <b>convienne</b>.</li>
</ul>

<p>
Now that I have a function for retrieving the HTML or Org Mode for a section, I can use that to wdiff against my current text to more easily spot wording changes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-lang-tutor-notes-wdiff-org</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">let</span> ((section (org-entry-get (point) <span class="org-string">"ITEM"</span>)))
    (my-wdiff-strings
     (replace-regexp-in-string
      <span class="org-string">"&#160;"</span> <span class="org-string">" "</span>
      (my-org-subtree-text-without-blocks))
     (replace-regexp-in-string
      <span class="org-string">"&#160;"</span> <span class="org-string">" "</span>
      (my-lang-tutor-notes section)))))
</code></pre>
</div>


<p>
Related:
</p>
<ul class="org-ul">
<li><code>my-wdiff-strings</code> is in <a href="https://sachachua.com/dotemacs#wdiff">Wdiff</a></li>
<li><code>my-org-subtree-text-without-blocks</code> is in <a href="https://sachachua.com/dotemacs#org-mode-publishing-counting-words-without-blocks">Counting words without blocks</a></li>
</ul>

<p>
Screenshot:
</p>


<figure id="org92c2ff5">
<a href="https://sachachua.com/blog/2026/03/emacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document/2026-03-12_11-28-24.png"><img src="https://sachachua.com/blog/2026/03/emacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document/2026-03-12_11-28-24.png" alt="2026-03-12_11-28-24.png"></a>

<figcaption><span class="figure-number">Figure 1: </span>wdiff</figcaption>
</figure>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#multimedia-learning-french">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/03/emacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document/index.org">View Org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F03%2Femacs-lisp-and-nodejs-getting-the-bolded-words-from-a-section-of-a-google-document%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>On this day</title>
		<link>https://sachachua.com/blog/2025/03/on-this-day/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 23 Mar 2025 16:02:57 GMT</pubDate>
    <category>11ty</category>
<category>js</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/03/on-this-day/</guid>
		<description><![CDATA[<p>
Nudged by <a href="https://github.com/emacsomancer/org-daily-reflection">org-daily-reflection</a> (<a href="https://types.pl/@emacsomancer/114096616526351883">@emacsomancer's
toot</a>) and <a href="https://adactio.com/journal/21811">Jeremy Keith's post</a> where he mentions
his <a href="https://adactio.com/archive/onthisday">on this day</a> page, I finally got around to
making my own <a href="https://sachachua.com/blog/on-this-day">on this day</a> page again. I use the
11ty static site generator, so it's static unless
you have Javascript enabled. It might be good for
<a href="https://sachachua.com/blog/2024/11/how-do-i-want-to-get-better-at-learning-out-loud-part-1-of-4-starting/#org1a9a0b0">bumping into things</a>. I used to have an <a href="https://sachachua.com/blog/2014/03/notes-managing-large-blog-archive/#:~:text=I%20have%20an%20%E2%80%9COn%20this%20day%E2%80%9D%20widget">"On this
day" widget</a> back when I used Wordpress, which was
fun to look at occasionally.
</p>

<p>
The code might be a little adamant about
converting all the dates to America/Toronto:
</p>

<details><summary>11ty code for posts on this day</summary>
<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">export</span> <span class="org-keyword">default</span> <span class="org-keyword">class</span> OnThisDay {
  data() {
    <span class="org-keyword">return</span> {
      layout: <span class="org-string">'layouts/base'</span>,
      permalink: <span class="org-string">'/blog/on-this-day/'</span>,
      title: <span class="org-string">'On this day'</span>
    };
  }

  <span class="org-keyword">async</span> render(data) {
    <span class="org-keyword">const</span> <span class="org-variable-name">today</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(<span class="org-keyword">new</span> <span class="org-type">Date</span>().toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> }));
    <span class="org-keyword">const</span> <span class="org-variable-name">options</span> = { month: <span class="org-string">'long'</span>, day: <span class="org-string">'numeric'</span> };
    <span class="org-keyword">const</span> <span class="org-variable-name">date</span> = today.toLocaleDateString(<span class="org-string">'en-US'</span>, options);
    <span class="org-keyword">const</span> <span class="org-variable-name">currentMonthDay</span> = today.toISOString().substring(5, 10);
    <span class="org-keyword">let</span> <span class="org-variable-name">list</span> = data.collections._posts
        .filter(post =&gt; {
          <span class="org-keyword">const</span> <span class="org-variable-name">postDateTime</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(post.date).toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> });
          <span class="org-keyword">const</span> <span class="org-variable-name">postMonthDay</span> = (<span class="org-keyword">new</span> <span class="org-type">Date</span>(postDateTime)).toISOString().substring(5, 10);
          <span class="org-keyword">return</span> postMonthDay === currentMonthDay;
        })
        .sort((a, b) =&gt; {
          <span class="org-keyword">if</span> (a.date &lt; b.date) <span class="org-keyword">return</span> 1;
          <span class="org-keyword">if</span> (a.date &gt; b.date) <span class="org-keyword">return</span> -1;
          <span class="org-keyword">return</span> 0;
        })
        .map(post =&gt; {
          <span class="org-keyword">const</span> <span class="org-variable-name">postDateTime</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(post.date).toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> });
          <span class="org-keyword">const</span> <span class="org-variable-name">postDate</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(postDateTime);
          <span class="org-keyword">const</span> <span class="org-variable-name">postYear</span> = postDate.getFullYear();
          <span class="org-keyword">return</span> <span class="org-string">`&lt;li&gt;${postYear}: &lt;a href="${post.url}"&gt;${post.data.title}&lt;/a&gt;&lt;/li&gt;`</span>;
        })
        .join(<span class="org-string">'\n'</span>);
    list = list.length &gt; 0
      ? <span class="org-string">`&lt;ul&gt;${list}&lt;/ul&gt;`</span>
      : <span class="org-string">`&lt;p&gt;No posts were written on ${date} in previous years.&lt;/p&gt;`</span>;

    <span class="org-keyword">return</span> <span class="org-string">`&lt;section&gt;&lt;h2&gt;On this day&lt;/h2&gt;</span>
<span class="org-string">&lt;p&gt;This page lists posts written on this day throughout the years. If you've enabled Javascript, it will show the current day. If you don't, it'll show the posts from the day I last updated this blog. You might also like to explore &lt;a href="/blog/all"&gt;all posts&lt;/a&gt;, &lt;a href="/topic"&gt;a topic-based outline&lt;/a&gt; or &lt;a href="/blog/category"&gt;categories&lt;/a&gt;.&lt;/p&gt;</span>
<span class="org-string">&lt;h3 class="date"&gt;${date}&lt;/h3&gt;</span>
<span class="org-string">&lt;div id="posts-container"&gt;${list}&lt;/div&gt;</span>

<span class="org-string">&lt;script&gt;</span>
<span class="org-string">  $(document).ready(function() { onThisDay(); });</span>
<span class="org-string">&lt;/script&gt;</span>
<span class="org-string">&lt;/section&gt;`</span>;
  }
};
</pre>
</div>

</details>

<details><summary>Client-side Javascript for the dynamic list</summary>
<div class="org-src-container">
<pre class="src src-js2">function onThisDay() {
  const tz = 'America/Toronto';
  function getEffectiveDate() {
    const urlParams = new URLSearchParams(window.location.search);
    const dateParam = urlParams.get('date');
    if (dateParam &amp;&amp; /^\d{2}-\d{2}$/.test(dateParam)) {
      const currentYear = new Date().getFullYear();
      const dateObj = new Date(`${currentYear}-${dateParam}T12:00:00Z`);
      if (dateObj.getTime()) {
        return {
          monthDay: dateParam,
          formatted: dateObj.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
        };
      }
    }
    const today = new Date(new Date().toLocaleString('en-US', { timeZone: tz }));
    return {
      monthDay: today.toISOString().substring(5, 10), // MM-DD
      formatted: today.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
    };
  }
  // Fetch and process the posts
  fetch('/blog/all/index.json')
    .then(response =&gt; response.json())
    .then(posts =&gt; {
      const dateInfo = getEffectiveDate();
      const dateElement = document.querySelector('h3.date');
      if (dateElement) {
        dateElement.textContent = dateInfo.formatted;
      }
      const matchingPosts = posts.filter(post =&gt; {
        const postDate = new Date(post.date).toLocaleString('en-US', { timeZone: tz });
        const postMonthDay = (new Date(postDate)).toISOString().substring(5, 10);
        return postMonthDay === dateInfo.monthDay;
      });

      matchingPosts.sort((a, b) =&gt; {
        const dateA = new Date(a.date);
        const dateB = new Date(b.date);
        return dateB - dateA;
      });

      const elem = document.getElementById('posts-container');
      if (matchingPosts.length &gt; 0) {
        const postsHTML = matchingPosts.map(post =&gt; {
          const postDate = new Date(post.date).toLocaleString('en-US', { timeZone: tz });
          const postYear = new Date(postDate).getFullYear();
          return `&lt;li&gt;${postYear}: &lt;a href="${post.permalink}"&gt;${post.title}&lt;/a&gt;&lt;/li&gt;`;
        }).join('\n');
        elem.innerHTML = `&lt;ul&gt;${postsHTML}&lt;/ul&gt;`;
      } else {
        elem.innerHTML = `&lt;p&gt;No posts were written on ${dateInfo.formatted}.&lt;/p&gt;`;
      }
    })
    .catch(error =&gt; {
      console.error('Error fetching posts:', error);
    });
}
</pre>
</div>

</details>

<p>
I used to include the day's posts as a footer on
the individual blog post page. That might be
something to consider again.
</p>
<div><a href="https://sachachua.com/blog/2025/03/on-this-day/index.org">View org source for this post</a></div>
<p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ1VKG7TBQXXZVSZS9X3JT3X" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fon-this-day%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Working with smaller chunks of thoughts; adding anchors to paragraphs in Org Mode HTML export</title>
		<link>https://sachachua.com/blog/2025/02/adding-an-anchor-to-a-paragraph-in-org-mode-html-export/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 25 Feb 2025 15:14:49 GMT</pubDate>
    <category>org</category>
<category>js</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/02/adding-an-anchor-to-a-paragraph-in-org-mode-html-export/</guid>
		<description><![CDATA[<div class="update" id="org5d3af93">
<ul class="org-ul">
<li><span class="timestamp-wrapper"><span class="timestamp">[2025-03-20 Thu]</span></span>: Fleshed out how to link to text fragments</li>
<li><span class="timestamp-wrapper"><span class="timestamp">[2025-02-28 Fri]</span></span>: Added link to <a href="https://sachachua.com/dotemacs#spookfox-fragment">elisp code to link to currently-selected text using Spookfox</a></li>
</ul>

</div>
<div class="sticky-toc" id="org7c53e98">
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-adding-anchors-to-paragraphs">Adding anchors to paragraphs</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-anchor-links">Anchor links</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-text-fragments">Text fragments</a></li>
</ul>
</div>

</div>

<p>
I write my blog posts in <a href="https://orgmode.org/">Org Mode</a> and export them
to <a href="https://www.11ty.dev/">Eleventy</a> with <a href="https://github.com/sachac/ox-11ty">ox-11ty</a>, which is derived from
the ox-html backend.
</p>

<p>
Sometimes I want to link to something in a
different blog post. This lets me build on
thoughts that are part of a post instead of being
a whole post on their own.
</p>

<p>
If I haven't added an anchor to the blog post yet,
I can add one so that I can link to that section.
For really old posts where I don't have an Org
source file, I can edit the HTML file directly and
add an <code>id="some-id"</code> so that I can link to it
with <code>/url/to/post#some-id</code>. Most of my new posts
have Org source, though. I have a
<code>my-blog-edit-org</code> function and a
<code>my-blog-edit-html</code> function in my <a href="https://sachachua.com/dotemacs">Emacs
configuration</a> to make it easier to jump to the Org
file or HTML for a blog post.
</p>

<p>
If the section has a heading, then it's easy to
make that linkable with a custom name. I can use
<code>org-set-property</code> to set the <code>CUSTOM_ID</code> property
to the anchor name. For example, this <a href="https://sachachua.com/blog/2025/01/controlling-my-android-phone-by-voice/#controlling-my-android-phone-by-voice-voice-access">voice access</a>
section has a heading that has <code>CUSTOM_ID</code>, as you
can see in the . If I don't mind having
long anchor names, I can use the
<code>my-assign-custom-ids</code> function from my config to
automatically set them based on the outline path.
</p>

<p>
</p><details><summary>my-assign-custom-ids</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-assign-custom-ids</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">let</span> ((custom-ids
         (org-map-entries (<span class="org-keyword">lambda</span> () (org-entry-get (point) <span class="org-string">"CUSTOM_ID"</span>)) <span class="org-string">"CUSTOM_ID={.}"</span>)))
    (org-map-entries
     (<span class="org-keyword">lambda</span> ()
       (<span class="org-keyword">let</span> ((slug
              (replace-regexp-in-string
               <span class="org-string">"^-</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string">-$"</span> <span class="org-string">""</span>
               (replace-regexp-in-string <span class="org-string">"[</span><span class="org-string"><span class="org-negation-char">^</span></span><span class="org-string">A-Za-z0-9]+"</span> <span class="org-string">"-"</span>
                                         (downcase (string-join (org-get-outline-path t) <span class="org-string">" "</span>))))))
         (<span class="org-keyword">while</span> (member slug custom-ids)
           (<span class="org-keyword">setq</span> slug (read-string <span class="org-string">"Manually set custom ID: "</span>)))
         (org-entry-put (point) <span class="org-string">"CUSTOM_ID"</span> slug)))
     <span class="org-string">"-CUSTOM_ID={.}"</span>)))

</pre></div></details>
<p></p>
<div id="outline-container-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-adding-anchors-to-paragraphs" class="outline-2">
<h3 id="working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-adding-anchors-to-paragraphs">Adding anchors to paragraphs</h3>
<div class="outline-text-2" id="text-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-adding-anchors-to-paragraphs">
<p>
If the part that I want to link to is not a
heading, I can add an ID by using the
<code>#+ATTR_HTML: :id ...</code> directive, like <a href="https://sachachua.com/blog/2025/02/looking-at-landscapes-art-and-iteration/#interest-development">this snippet from my reflection on landscapes and art</a>:
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">  #+ATTR_HTML: :id interest-development</span>
  That reminds me a little of another reflection
  I've been noodling around on interest development...
</pre>
</div>

</div>
</div>
<div id="outline-container-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-anchor-links" class="outline-2">
<h3 id="working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-anchor-links">Anchor links</h3>
<div class="outline-text-2" id="text-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-anchor-links">
<p>
It might be fun to have a little margin note with
🔗 to indicate that that's a specially-linkable
section, which could be handy when I want to link
when mobile. It feels like that would be a left
margin thing on a large screen, so it'll just have
to squeeze in there with the sticky table of
contents. I've been meaning to add link icons to
sub-headings with IDs, anyway, so I can probably solve
both with a bit of Javascript.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-comment-delimiter">/* </span><span class="org-comment">Add link icons to headings and anchored paragraphs</span><span class="org-comment-delimiter"> */</span>
<span class="org-keyword">function</span> <span class="org-function-name">addLinkIcons</span>() {
  document.querySelectorAll(<span class="org-string">'h1[id], h2[id], h3[id], p[id]'</span>).forEach((o) =&gt; {
    <span class="org-keyword">const</span> <span class="org-variable-name">link</span> = document.createElement(<span class="org-string">'a'</span>);
    <span class="org-keyword">const</span> <span class="org-variable-name">article</span> = o.closest(<span class="org-string">'article[data-url]'</span>);
    link.href = window.location.origin + (article?.getAttribute(<span class="org-string">'data-url'</span>) || window.location.pathname) + <span class="org-string">'#'</span> + o.getAttribute(<span class="org-string">'id'</span>);
    link.innerHTML = <span class="org-string">'&amp;#x1F517;'</span>;  <span class="org-comment-delimiter">// </span><span class="org-comment">link icon</span>
    link.title = <span class="org-string">'anchor'</span>;
    link.classList.add(<span class="org-string">'anchor-icon'</span>);
    link.addEventListener(<span class="org-string">'click'</span>, copyLink);
    o.prepend(link);
  });
}

addLinkIcons();
</pre>
</div>


<p>
And some CSS:
</p>


<div class="org-src-container">
<pre class="src src-css"><span class="org-css-selector">.entry h2, .entry h3, .entry h4, .entry p, .content h2, .content h3, .content h4, .content p</span> { <span class="org-css-property">position</span>: relative }
<span class="org-css-selector">.content a.anchor-icon:link, .entry a.anchor-icon:link</span> { <span class="org-css-property">font-size</span>: 60%; <span class="org-css-property">text-decoration</span>: none <span class="org-builtin">!important</span>; <span class="org-css-property">font-size</span>: small; <span class="org-css-property">float</span>: right }
<span class="org-builtin">@media</span> only screen and (min-width: 95rem) {
   <span class="org-css-selector">.content a.anchor-icon:link, .entry a.anchor-icon:link</span> { <span class="org-css-property">display</span>: block; <span class="org-css-property">position</span>: absolute; <span class="org-css-property">left</span>: -2em; <span class="org-css-property">top</span>: 0.2em; <span class="org-css-property">float</span>: none}
}
</pre>
</div>

</div>
</div>
<div id="outline-container-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-text-fragments" class="outline-2">
<h3 id="working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-text-fragments">Text fragments</h3>
<div class="outline-text-2" id="text-working-with-smaller-chunks-of-thoughts-adding-anchors-to-paragraphs-in-org-mode-html-export-text-fragments">
<p>
<a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments">Text fragments</a> are even more powerful, because I
can link to a specific part of a paragraph. I can
link to one segment with something like
<code>#:</code>:text=text+to+highlight~. I can specify
multiple text fragments to highlight by using
<code>#:</code>:text=first+text+to+highlight&amp;text=second+text~,
and the browser will automatically scroll to the
first highlighted section. I can specify a longer
section by using text=textStart,textEnd. Example:
<a href="https://sachachua.com/blog/2025/02/looking-at-landscapes-art-and-iteration/#:~:text=That%20is%20the%20gap,described">#:~:text=That%20is%20the%20gap,described</a> The <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments">text
fragments documentation</a> has more options,
including using prefixes and suffixes to
disambiguate matches.
</p>

<p>
Text fragment links require <code>rel="noopener"</code> for
security, so I added
<a href="https://github.com/JKC-Codes/eleventy-plugin-automatic-noopener">JKC-Codes/eleventy-plugin-automatic-noopener</a> to my
11ty config.
</p>

<p>
Update 2025-03-20: Quick ways to link to a text fragment:
</p>

<ul class="org-ul">
<li>On my Android phone, selecting text in Google Chrome and sharing it automatically includes the text and a link to the text fragment.</li>
<li>In Google Chrome on my iPad, my process is:
<ol class="org-ol">
<li>Select the text and choose "Copy Link with Highlight".</li>
<li>Tap the selected text again and share it.</li>
<li>Paste the link after the shared text.</li>
</ol></li>
<li>There's this <a href="https://addons.mozilla.org/en-US/firefox/addon/text-fragment/">Text Fragment extension for Firefox</a>.</li>
<li>I have <a href="https://sachachua.com/dotemacs#spookfox-fragment">some Emacs Lisp to link to currently-selected text using Spookfox</a>. <a href="https://github.com/bitspook/spookfox">Spookfox</a> connects Emacs to Firefox using a browser extension. Once it's properly set up and connected, it allows Emacs to evaluate things in the Firefox context.</li>
</ul>

<p>
These seem like good starting points for
addressing smaller chunks of thoughts.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2025/02/adding-an-anchor-to-a-paragraph-in-org-mode-html-export/index.org">View org source for this post</a></div>

<p>You can <a href="https://sachachua.com/blog/2025/02/adding-an-anchor-to-a-paragraph-in-org-mode-html-export/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F02%2Fadding-an-anchor-to-a-paragraph-in-org-mode-html-export%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Checking caption timing by skimming with Emacs Lisp or JS</title>
		<link>https://sachachua.com/blog/2024/11/checking-caption-timing-by-skimming-with-emacs-lisp-or-js/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 17 Nov 2024 12:29:02 GMT</pubDate>
    <category>js</category>
<category>emacs</category>
<category>subed</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/11/checking-caption-timing-by-skimming-with-emacs-lisp-or-js/</guid>
		<description><![CDATA[<p>
Sometimes automatic subtitle timing tools like
<a href="https://www.readbeyond.it/aeneas/">Aeneas</a> can get confused by silences, extraneous
sounds, filler words, mis-starts, and text that
I've edited out of the raw captions for easier
readability. It's good to quickly check each
caption. I used to listen to captions at 1.5x
speed, watching carefully as each caption
displayed. This took a fair bit of time and focus,
so&#x2026; it usually didn't happen. Sampling the first
second of each caption is faster and requires a
little less attention.
</p>
<div id="outline-container-orgdc21377" class="outline-2">
<h3 id="orgdc21377">Skimming with subed.el</h3>
<div class="outline-text-2" id="text-orgdc21377">
<p>
Here's a function that I wrote to play the first
second of each subtitle.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-subed-skim-msecs</span> 1000 <span class="org-doc">"Number of milliseconds to play when skimming."</span>)
(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-skim-starts</span> ()
  (<span class="org-keyword">interactive</span>)
  (subed-mpv-unpause)
  (subed-disable-loop-over-current-subtitle)
  (<span class="org-keyword">catch</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">done</span>
    (<span class="org-keyword">while</span> (not (eobp))
      (subed-mpv-jump-to-current-subtitle)
      (<span class="org-keyword">let</span> ((ch
             (read-char <span class="org-string">"(q)uit? "</span> nil (/ my-subed-skim-msecs 1000.0))))
        (<span class="org-keyword">when</span> ch
          (<span class="org-keyword">throw</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">done</span> t)))
      (subed-forward-subtitle-time-start)
      (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> subed-waveform-minor-mode
                 (not subed-waveform-show-all))
        (subed-waveform-refresh))
      (recenter)))
  (subed-mpv-pause))
</pre>
</div>


<p>
Now I can read the lines as the subtitles play,
and I can press any key to stop so that I can fix
timestamps.
</p>
</div>
</div>
<div id="outline-container-org8f02356" class="outline-2">
<h3 id="org8f02356">Skimming with Javascript</h3>
<div class="outline-text-2" id="text-org8f02356">
<p>
I also want to check the times on the Web in case
there have been caching issues. Here's some
Javascript to skim the first second of each cue in
the first text track for a video, with some code
to make it easy to process the first video in the
visible area.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">function getVisibleVideo() {
  const videos = document.querySelectorAll(<span class="org-highlight-quoted-quote">'</span><span class="org-constant">video</span><span class="org-highlight-quoted-quote">'</span>)<span class="org-comment-delimiter">;</span>
  for (const video of videos) {
    const rect = video.getBoundingClientRect()<span class="org-comment-delimiter">;</span>
    if (
      rect.top &gt;= 0 <span class="org-type">&amp;&amp;</span>
      rect.left &gt;= 0 <span class="org-type">&amp;&amp;</span>
      rect.bottom &lt;= (window.innerHeight || document.documentElement.clientHeight) <span class="org-type">&amp;&amp;</span>
      rect.right &lt;= (window.innerWidth || document.documentElement.clientWidth)
    ) <span class="org-warning">{</span>
      return video<span class="org-comment-delimiter">;</span>
    }
  }
  return null<span class="org-comment-delimiter">;</span>
}

async function skimVideo(video=getVisibleVideo(), msecs=1000) {
  // Get the first text track (assumed to be captions/subtitles)
  const textTrack = video.textTracks[0]<span class="org-comment-delimiter">;</span>
  if (!textTrack) return<span class="org-comment-delimiter">;</span>
  const remaining = [...textTrack.cues].filter((cue) =&gt; cue.endTime &gt;= video.currentTime)<span class="org-comment-delimiter">;</span>
  video.play()<span class="org-comment-delimiter">;</span>
  // Play the first 1 second of each visible subtitle
  for (<span class="org-keyword">let</span> i = 0<span class="org-comment-delimiter">; </span><span class="org-comment">i &lt; remaining.length &amp;&amp; !video.paused; i++) {</span>
    video.currentTime = remaining[i].startTime<span class="org-comment-delimiter">;</span>
    await new Promise((resolve) =&gt; setTimeout(resolve, msecs))<span class="org-comment-delimiter">;</span>
  }
}
</pre>
</div>


<p>
Then I can call it with <code>skimVideo();</code>. Actually,
in our backstage area, it might be useful to add a
Skim button so that I can skim things from my
phone.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">function</span> <span class="org-function-name">handleSkimButton</span>(<span class="org-variable-name">event</span>) {
   <span class="org-keyword">const</span> <span class="org-variable-name">vid</span> = event.target.closest(<span class="org-string">'.vid'</span>).querySelector(<span class="org-string">'video'</span>);
   skimVideo(vid);
 }

document.querySelectorAll(<span class="org-string">'video'</span>).forEach((vid) =&gt; {
   <span class="org-keyword">const</span> <span class="org-variable-name">div</span> = document.createElement(<span class="org-string">'div'</span>);
   <span class="org-keyword">const</span> <span class="org-variable-name">skim</span> = document.createElement(<span class="org-string">'button'</span>);
   skim.textContent = <span class="org-string">'Skim'</span>;
   div.appendChild(skim);
   vid.parentNode.insertBefore(div, vid.nextSibling);
   skim.addEventListener(<span class="org-string">'click'</span>, handleSkimButton);
});
</pre>
</div>

</div>
</div>
<div id="outline-container-orgf22ecd7" class="outline-2">
<h3 id="orgf22ecd7">Results</h3>
<div class="outline-text-2" id="text-orgf22ecd7">
<p>
How much faster is it this way?
</p>

<details><summary>Some code to help figure out the speedup</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">-let*</span> ((files (directory-files <span class="org-string">"~/proj/emacsconf/2024/cache"</span> t <span class="org-string">"&#45;&#45;main\\.vtt"</span>))
        ((count-subs sum-seconds)
         (-unzip (mapcar
                  (<span class="org-keyword">lambda</span> (file)
                    (list
                     (length (subed-parse-file file))
                     (/ (compile-media-get-file-duration-ms
                         (concat (file-name-sans-extension file) <span class="org-string">".webm"</span>)) <span class="org-warning">1000.0)))</span>
                  files)))
        (total-seconds (-reduce <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">+</span> sum-seconds))
        (total-subs (-reduce <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">+</span> count-subs)))
  (format <span class="org-string">"%d files, %.1f hours, %d total captions, speed up of %.1f"</span>
          (length files)
          (/ total-seconds 3600.0)
          total-subs
          (/ total-seconds total-subs)))
</pre>
</div>

</details>

<p>
It looks like for EmacsConf talks where we
typically format captions to be one long line each
(&lt; 60 characters), this can be a speed-up of about
4x compared to listening to the video at normal
speed. More usefully, it's different enough to get
my brain to do it instead of putting it off.
</p>

<p>
Most of the automatically-generated timestamps are
fine. It's just a few that might need tweaking.
It's nice to be able to skim them with fewer
keystrokes.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/11/checking-caption-timing-by-skimming-with-emacs-lisp-or-js/index.org">View org source for this post</a></div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F11%2Fchecking-caption-timing-by-skimming-with-emacs-lisp-or-js%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Using Javascript to add a "Copy code" link to source code blocks in my blog posts</title>
		<link>https://sachachua.com/blog/2023/01/using-javascript-to-add-a-copy-code-link-to-source-code-blocks-in-my-blog-posts/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 07 Jan 2023 20:32:31 GMT</pubDate>
    <category>css</category>
<category>js</category>
<category>blogging</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/01/using-javascript-to-add-a-copy-code-link-to-source-code-blocks-in-my-blog-posts/</guid>
		<description><![CDATA[<p>
I'd like to write about code more often. It's easier for people to try
out ideas if they can copy the code without fiddling with selecting
the text, especially on mobile browsers, so "Copy code" buttons on
source code blocks would be nice. I used <a href="https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html">this tutorial for adding code buttons</a> as a basis for the following CSS and JS code.
</p>

<p>
First, let's add the buttons with Javascript. I want the buttons to be
visible in the summary line if I'm using the <code>&lt;details /&gt;</code> element. If
not, they can go in the div with the <code>org-src-container</code> class.
</p>

<p>
</p><div class="org-src-container">
<pre class="src src-js"><span class="org-comment-delimiter">/* </span><span class="org-comment">Start of copy code</span><span class="org-comment-delimiter"> */</span>
<span class="org-comment-delimiter">// </span><span class="org-comment">based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html</span>
<span class="org-keyword">const</span> <span class="org-variable-name">copyLabel</span> = <span class="org-string">'Copy code'</span>;

<span class="org-keyword">async</span> <span class="org-keyword">function</span> copyCode(<span class="org-variable-name">block</span>, <span class="org-variable-name">button</span>) {
  <span class="org-keyword">let</span> <span class="org-variable-name">code</span> = block.querySelector(<span class="org-string">'pre.src'</span>);
  <span class="org-keyword">let</span> <span class="org-variable-name">text</span> = code.innerText;
  <span class="org-keyword">await</span> navigator.clipboard.writeText(text);
  button.innerText = <span class="org-string">'Copied'</span>;
  setTimeout(() =&gt; {
    button.innerText = copyLabel;
  }, 500);
}

<span class="org-keyword">function</span> <span class="org-function-name">addCopyCodeButtons</span>() {
  <span class="org-keyword">if</span> (!navigator.clipboard) <span class="org-keyword">return</span>;
  <span class="org-keyword">let</span> <span class="org-variable-name">blocks</span> = document.querySelectorAll(<span class="org-string">'.org-src-container'</span>);
  blocks.forEach((block) =&gt; {
    <span class="org-keyword">let</span> <span class="org-variable-name">button</span> = document.createElement(<span class="org-string">'button'</span>);
    button.innerText = copyLabel;
    button.classList.add(<span class="org-string">'copy-code'</span>);
    <span class="org-keyword">let</span> <span class="org-variable-name">details</span> = block.closest(<span class="org-string">'details'</span>);
    <span class="org-keyword">let</span> <span class="org-variable-name">summary</span> = details &amp;&amp; details.querySelector(<span class="org-string">'summary'</span>);
    <span class="org-keyword">if</span> (summary) {
      summary.appendChild(button);
    } <span class="org-keyword">else</span> {
      block.appendChild(button);
    }
    button.addEventListener(<span class="org-string">'click'</span>, <span class="org-keyword">async</span>() =&gt; {
      <span class="org-keyword">await</span> copyCode(block, button);
    });
    block.setAttribute(<span class="org-string">'tabindex'</span>, 0);
  });
}
document.addEventListener(<span class="org-string">"DOMContentLoaded"</span>, <span class="org-keyword">function</span>(<span class="org-variable-name">event</span>) { 
  addCopyCodeButtons();
});
<span class="org-comment-delimiter">/* </span><span class="org-comment">End of copy code</span><span class="org-comment-delimiter"> */</span>
</pre>
</div>

<p></p>

<p>
Then we style it:
</p>

<p>
</p><div class="org-src-container">
<pre class="src src-css"><span class="org-comment-delimiter">/* </span><span class="org-comment">Start of copy code</span><span class="org-comment-delimiter"> */</span>
<span class="org-css-selector">pre.src</span> { <span class="org-css-property">margin</span>: 0 }
<span class="org-css-selector">.org-src-container</span> {
    <span class="org-css-property">position</span>: relative;
    <span class="org-css-property">margin</span>: 0 0;
    <span class="org-css-property">padding</span>: 1.75rem 0 1.75rem 1rem;
}
<span class="org-css-selector">summary</span> { <span class="org-css-property">position</span>: relative; }
<span class="org-css-selector">summary .org-src-container</span> { <span class="org-css-property">padding</span>: 0 }
<span class="org-css-selector">summary .org-src-container pre.src</span> { <span class="org-css-property">margin</span>: 0 }
<span class="org-css-selector">.org-src-container button.copy-code, summary button.copy-code</span> {
    <span class="org-css-property">position</span>: absolute;
    <span class="org-css-property">top</span>: 0px;
    <span class="org-css-property">right</span>: 0px;
}
<span class="org-comment-delimiter">/* </span><span class="org-comment">End of copy code</span><span class="org-comment-delimiter"> */</span>
</pre>
</div>

<p></p>

<p>
Someday I'll figure out how to make it easier to tangle things to the
post's directory and make the file available for download. In the
meantime, this might be a good start.
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F01%2Fusing-javascript-to-add-a-copy-code-link-to-source-code-blocks-in-my-blog-posts%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>