rework sidenotes to make nesting possible

This commit is contained in:
Joseph Montanaro 2024-07-11 06:03:34 -04:00
parent 716792e8a6
commit 9eeb3e87bd
4 changed files with 99 additions and 55 deletions

3
.gitignore vendored
View File

@ -8,4 +8,5 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
**/_test.* **/_test.*
/scratch

View File

@ -8,31 +8,36 @@
counter-reset: sidenote; counter-reset: sidenote;
} }
.counter { .counter.anchor {
counter-increment: sidenote;
color: #444; color: #444;
margin-left: 0.065rem; margin-left: 0.065rem;
font-size: 0.75em;
&::after { position: relative;
font-size: 0.75em; bottom: 0.375rem;
position: relative; color: var(--accent-color);
bottom: 0.375rem;
color: var(--accent-color);
content: counter(sidenote);
}
@media(max-width: $sidenote-breakpoint) { @media(max-width: $sidenote-breakpoint) {
&::after { &:hover {
content: "[" counter(sidenote) "]";
}
&:hover::after {
color: var(--content-color); color: var(--content-color);
cursor: pointer; cursor: pointer;
} }
// only top-level anchors get brackets
&:not(.nested)::before {
content: '[';
}
&:not(.nested)::after {
content: ']';
}
} }
} }
.counter.floating {
position: absolute;
transform: translateX(calc(-100% - 0.4em));
color: var(--accent-color);
}
// hidden checkbox that tracks the state of the mobile sidenote // hidden checkbox that tracks the state of the mobile sidenote
.sidenote-toggle { .sidenote-toggle {
display: none; display: none;
@ -50,7 +55,7 @@
@media(min-width: $sidenote-breakpoint) { @media(min-width: $sidenote-breakpoint) {
// max sidenote width is 20rem, if the window is too small then it's // max sidenote width is 20rem, if the window is too small then it's
// the width of the gutter, minus the gap between sidenote and gutter, // the width of the gutter, minus the gap between sidenote and gutter,
// minus an extra 1.5rem to account for the scrollbar on the right // minus an extra 1.5rem to account for the scrollbar on the right
--gap: 2.5rem; --gap: 2.5rem;
--gutter-width: calc(50vw - var(--content-width) / 2); --gutter-width: calc(50vw - var(--content-width) / 2);
--sidenote-width: min( --sidenote-width: min(
@ -64,7 +69,7 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@media(max-width: $sidenote-breakpoint) { @media(max-width: $sidenote-breakpoint) {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;
@ -88,6 +93,9 @@
transform: translateY(0); transform: translateY(0);
// when moving hidden -> shown, ease-out // when moving hidden -> shown, ease-out
transition-timing-function: ease-out; transition-timing-function: ease-out;
// the active sidenote should be on top of any other sidenotes as well
// (this isn't critical unless you have JS disabled, but it's still annoying)
z-index: 20;
} }
} }
} }
@ -96,11 +104,11 @@
max-width: var(--content-width); max-width: var(--content-width);
margin: 0 auto; margin: 0 auto;
&::before {
position: absolute; &.nested {
transform: translateX(calc(-100% - 0.4em)); margin-right: 0;
content: counter(sidenote); margin-top: 0.75rem;
color: var(--accent-color); margin-bottom: 0;
} }
} }
@ -137,65 +145,75 @@
} }
// nesting still needs work // nesting still needs work
@media(min-width: $sidenote-breakpoint) { /* @media(min-width: $sidenote-breakpoint) {
.nested.sidenote { .nested.sidenote {
margin-right: 0; margin-right: 0;
margin-top: 0.7rem; margin-top: 0.7rem;
margin-bottom: 0; margin-bottom: 0;
} }
} } */
</style> </style>
<script context="module"> <script context="module">
var activeToggle = null; import { writable } from 'svelte/store';
let activeSidenote = writable(null);
</script> </script>
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let count;
let noteBody; let noteBody;
let nested = false; let nested = false;
onMount(() => { onMount(() => {
// check to see if the parent node is also a sidenote, if so move this one to the end // check to see if the parent node is also a sidenote, if so move this one to the end
let parentNote = noteBody.parentElement.closest('span.sidenote'); let parentContent = noteBody.parentElement.closest('div.sidenote-content');
if (parentNote) { if (parentContent) {
// extract just the content of the nested note, ditch the rest (i.e. the button)
const noteContent = noteBody.firstChild;
noteBody.remove(); noteBody.remove();
parentNote.appendChild(noteBody); parentContent.appendChild(noteContent);
nested = true; nested = true;
} }
}); });
const id = Math.random().toString().slice(2);
let toggle; let toggle;
activeSidenote.subscribe(activeCount => {
// if we were the active toggle, but are no longer, hide
if (toggle?.checked && activeCount !== count) {
toggle.checked = false;
}
})
function toggleState() { function toggleState() {
if (activeToggle === toggle) { // if we are the active sidenote, deactivate us (upating the store will trigger subscription)
activeToggle = null; if ($activeSidenote === count) {
} $activeSidenote = null;
else if (activeToggle !== null) {
activeToggle.checked = false;
activeToggle = toggle;
} }
// otherwise, we are becoming active
else { else {
activeToggle = toggle; $activeSidenote = count;
} }
} }
</script> </script>
<label for={id} on:click={toggleState} class="counter"></label> <label for={count} class="counter anchor" class:nested>{count}</label>
<input {id} bind:this={toggle} type="checkbox" class="sidenote-toggle" /> <input id={count} bind:this={toggle} on:click={toggleState} type="checkbox" class="sidenote-toggle" />
<!-- outer element so that on mobile it can extend the whole width of the viewport --> <!-- outer element so that on mobile it can extend the whole width of the viewport -->
<div class="sidenote" class:nested bind:this={noteBody}> <div class="sidenote" bind:this={noteBody}>
<!-- inner element so that content can be centered --> <!-- inner element so that content can be centered -->
<div class="sidenote-content"> <div class="sidenote-content" class:nested>
<span class="counter floating">{count}</span>
<slot></slot> <slot></slot>
<button class="dismiss" on:click={toggleState}>
<label for={id}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</label>
</button>
</div> </div>
</div> <button class="dismiss">
<label for={count}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</label>
</button>
</div>

View File

@ -12,6 +12,7 @@ export function localRehype() {
const needsDropcap = vfile.data.fm.dropcap !== false const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false; let dropcapAdded = false;
let sidenotesCount = 0;
let moduleScript; let moduleScript;
let imports = new Set(); let imports = new Set();
if (needsDropcap) { if (needsDropcap) {
@ -35,7 +36,13 @@ export function localRehype() {
if (needsDropcap && !dropcapAdded && isParagraph(node)) { if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node); addDropcap(node);
dropcapAdded = true; dropcapAdded = true;
return SKIP; }
// add `count` prop to each <Sidenote> component
if (isSidenote(node)) {
// increment the counter first so that the count starts at 1
sidenotesCount += 1;
addSidenoteCount(node, sidenotesCount);
} }
}); });
@ -52,6 +59,9 @@ export function localRehype() {
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`; moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
} }
// const name = vfile.filename.split('/').findLast(() => true);
// writeFileSync(`scratch/${name}.json`, JSON.stringify(tree, undefined, 4));
} }
} }
@ -78,6 +88,17 @@ function addDropcap(par) {
} }
function addSidenoteCount(node, count) {
// get the index of the closing >
const i = node.value.search(/>\s*$/);
if (i < 0) {
throw new Error('Failed to add counter to element, closing angle bracket not found.');
}
// splice in the count prop
node.value = `${node.value.slice(0, i)} count={${count}}>`;
}
function isHeading(node) { function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/); return node.type === 'element' && node.tagName.match(/h[1-6]/);
} }
@ -89,3 +110,7 @@ function isModuleScript(node) {
function isParagraph(node) { function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p'; return node.type === 'element' && node.tagName === 'p';
} }
function isSidenote(node) {
return node.type === 'raw' && node.value.match(/<\s*Sidenote/);
}

View File

@ -7,13 +7,13 @@ date: 2024-07-06
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '$lib/Sidenote.svelte';
</script> </script>
Like a lot of people, my main experience with private keys has come from using them for SSH. I'm familiar with the theory, of course - I know generally what asymmetric encryption does,<Sidenote>Although exactly _how_ it does so is still a complete mystery to me. I've looked up descriptions of RSA several times, and even tried to work my way through a toy example, but it's never helped. And I couldn't even _begin_ to explain elliptic curve cryptography beyond "black math magic".</Sidenote> and I know that it means a compromised server can't reveal your private key, which is nice although if you only ever use a given private key to SSH into your server and the server is already compromised, is that really so helpful?<Sidenote>Yes, yes, I know that it means you can use the same private key for _multiple_ things without having to worry, but in practice a lot of people seem to use separate private keys for separate things, and even though I'm not entirely sure why I feel uncomfortable doing otherwise.</Sidenote> Like a lot of people, my main experience with private keys has come from using them for SSH. I'm familiar with the theory, of course - I know generally what asymmetric encryption does,<Sidenote>Although exactly _how_ it does so is still a complete mystery to me. I've looked up descriptions of RSA several times,<Sidenote>Testing nested notes again.</Sidenote> and even tried to work my way through a toy example, but it's never helped. And I couldn't even _begin_ to explain elliptic curve cryptography beyond "black math magic".</Sidenote> and I know that it means a compromised server can't reveal your private key, which is nice although if you only ever use a given private key to SSH into your server and the server is already compromised, is that really so helpful?<Sidenote>Yes, yes, I know that it means you can use the same private key for _multiple_ things without having to worry, but in practice a lot of people seem to use separate private keys for separate things, and even though I'm not entirely sure why I feel uncomfortable doing otherwise.</Sidenote>
What I was less aware of, however, was the various ways in which private keys can be _stored_, which rather suddenly became a more-than-purely-academic concern to me this past week. I had an old private key lying around which had originally been generated by AWS, and used a rather old format,<Sidenote>The oldest, I believe, that's in widespread use still.</Sidenote> and I needed it to be comprehensible by newer software which loftily refused to have anything to do with such outdated ways of expressing itself.<Sidenote>Who would write such obdurately high-handed software, you ask? Well, uh. Me, as it turns out. In my defense, though, I doubt it would have taken _less_ time to switch to a different SSH-key library than to figure out the particular magic incantation needed to get `ssh-keygen` to do it.</Sidenote> No problem, thought I, I'll just use `ssh-keygen` to convert the old format to a newer format! Unfortunately this was frustratingly<Sidenote>And needlessly, it seems to me?</Sidenote> difficult to figure out, so I'm writing it up here for posterity and so that I never have to look it up again.<Sidenote>You know how it works. Once you've taken the time to really describe process in detail, you have it locked in and never have to refer back to your notes.</Sidenote> What I was less aware of, however, was the various ways in which private keys can be _stored_, which rather suddenly became a more-than-purely-academic concern to me this past week. I had an old private key lying around which had originally been generated by AWS, and used a rather old format,<Sidenote>The oldest, I believe, that's in widespread use still.</Sidenote> and I needed it to be comprehensible by newer software which loftily refused to have anything to do with such outdated ways of expressing itself.<Sidenote>Who would write such obdurately high-handed software, you ask? Well, uh. Me, as it turns out. In my defense, though, I doubt it would have taken _less_ time to switch to a different SSH-key library than to figure out the particular magic incantation needed to get `ssh-keygen` to do it.</Sidenote> No problem, thought I, I'll just use `ssh-keygen` to convert the old format to a newer format! Unfortunately this was frustratingly<Sidenote>And needlessly, it seems to me?</Sidenote> difficult to figure out, so I'm writing it up here for posterity and so that I never have to look it up again.<Sidenote>You know how it works. Once you've taken the time to really describe process in detail, you have it locked in and never have to refer back to your notes.</Sidenote>
## Preamble: Fantastic Formats and Where to Find Them ## Preamble: Fantastic Formats and Where to Find Them
If you're like me, you're probably aware that private keys are usually delivered as big blobs of Base64-encoded text prefaced by headers like `-----BEGIN OPENSSH PRIVATE KEY----`, and for some reason never use file extensions.<Sidenote>Well, for the ones generated by `ssh-keygen`, at least. OpenSSL-generated ones often use `.key` or `.pem`, but those aren't typically used for SSH, so are less relevant here.</Sidenote> There are three common formats you're likely to encounter in the wild: I was aware, of course, that private keys are usually delivered as files containing big blobs of Base64-encoded text, prefaced by headers like `-----BEGIN OPENSSH PRIVATE KEY----`, and for whatever reason lacking file extensions.<Sidenote>Well, for the ones generated by `ssh-keygen`, at least. OpenSSL-generated ones often use `.key` or `.pem`, but those aren't typically used for SSH, so are less relevant here.</Sidenote> What I wasn't aware of is that there are actually several _different_ such formats, which although they look quite similar from the outside are internally pretty different. There are three you're likely to encounter in the wild:
1. OpenSSH-formatted keys, which start with `BEGIN OPENSSH KEY`<Sidenote>Plus the leading and trailing five dashes, but I'm tired of typing those out.</Sidenote> and are the preferred way of formatting private keys for use with SSH, 1. OpenSSH-formatted keys, which start with `BEGIN OPENSSH KEY`<Sidenote>Plus the leading and trailing five dashes, but I'm tired of typing those out.</Sidenote> and are the preferred way of formatting private keys for use with SSH,
2. PCKS#8-formatted keys, which start with `BEGIN PRIVATE KEY` or `BEGIN ENCRYPTED PRIVATE KEY`, and 2. PCKS#8-formatted keys, which start with `BEGIN PRIVATE KEY` or `BEGIN ENCRYPTED PRIVATE KEY`, and
@ -37,7 +37,7 @@ Both PKCS#1 and PKCS#8 use the same method for encoding the actual key data (whi
## The `ssh-keygen` Manpage is a Tapestry of Lies ## The `ssh-keygen` Manpage is a Tapestry of Lies
So, I thought, I can use `ssh-keygen` to convert between these various and sundry formats, right? It can do that, it _has_ to be able to do that, right? So, I thought, I can use `ssh-keygen` to convert between these various and sundry formats, right? It can do that, it _has_ to be able to do that, right?
Well, yes. It _can_, but good luck figuring out _how_. For starters, like many older CLI tools, `ssh-keygen` has an awful lot of flags and options, and it's hard to distinguish between which are _modifiers_ - "do the same thing, but differently" - and _modes of operation_ - "do a different thing entirely". The modern way to handle this distinction is with subcommands which take entirely different sets of arguments, but `ssh-keygen` dates back to a time before that was common. Well, yes. It _can_, but good luck figuring out _how_. For starters, like many older CLI tools, `ssh-keygen` has an awful lot of flags and options, and it's hard to distinguish between which are _modifiers_ - "do the same thing, but differently" - and _modes of operation_ - "do a different thing entirely". The modern way to handle this distinction is with subcommands which take entirely different sets of arguments, but `ssh-keygen` dates back to a time before that was common.
@ -75,7 +75,7 @@ Most probably this is just a case of a tool evolving organically over time rathe
## Imagining a Brighter Tomorrow ## Imagining a Brighter Tomorrow
But it doesn't have to be this way! Nothing (that I can see) prevents the `-i` option from being updated to accept private keys as well as public keys: it's clearly perfectly capable of telling when a file _isn't_ a valid _public_ key in the specified format, so it it seems like it could just parse it as a private key instead, and keep going if successful. Or an entirely new option could be added for converting private keys. `-c` is already taken for changing comments, but there are a few letters remaining. I don't see a `-j` on the manpage, for instance, or an `-x`. But it doesn't have to be this way! Nothing (that I can see) prevents the `-i` option from being updated to accept private keys as well as public keys: it's clearly perfectly capable of telling when a file _isn't_ a valid _public_ key in the specified format, so it it seems like it could just parse it as a private key instead, and keep going if successful. Or an entirely new option could be added for converting private keys. `-c` is already taken for changing comments, but there are a few letters remaining. I don't see a `-j` on the manpage, for instance, or an `-x`.
I realize that an unforgiving reading of my travails in this endeavour might yield the conclusion that I'm an idiot with no reading comprehension, and that the manpage _clearly_ stated the solution to my problem _all along_, and if I had just RTFM<Sidenote>Noob.</Sidenote> then I could have avoided all this frustration, but that seems [a little unfair](https://xkcd.com/293/) to me.<Sidenote>Besides, I'm annoyed, and it's more satisfying to blame others than admit any fault of my own.</Sidenote> When you're writing the help message or manpage for your tool, you should _expect_ that people will be skimming it, looking to pick out the tidbits that are important to them right now, since for any tool of reasonable complexity 95% of the documentation is going to be irrelevant to any single user in any single situation. I realize that an unforgiving reading of my travails in this endeavour might yield the conclusion that I'm an idiot with no reading comprehension, and that the manpage _clearly_ stated the solution to my problem _all along_, and if I had just RTFM<Sidenote>Noob.</Sidenote> then I could have avoided all this frustration, but that seems [a little unfair](https://xkcd.com/293/) to me.<Sidenote>Besides, I'm annoyed, and it's more satisfying to blame others than admit any fault of my own.</Sidenote> When you're writing the help message or manpage for your tool, you should _expect_ that people will be skimming it, looking to pick out the tidbits that are important to them right now, since for any tool of reasonable complexity 95% of the documentation is going to be irrelevant to any single user in any single situation.