/**
* Based on https://edwardtufte.github.io/tufte-css/#sidenotes
*/
import type MarkdownIt from "npm:markdown-it@^14.1.0";
import type StateInline from "npm:markdown-it@^14.1.0/lib/rules_inline/state_inline.mjs";
import type { Options as MarkdownItOptions } from "npm:markdown-it@^14.1.0";
import type Renderer from "npm:markdown-it@^14.1.0/lib/renderer.mjs";
import type Token from "npm:markdown-it@^14.1.0/lib/token.mjs";
export default function tufteNotesPlugin(md: MarkdownIt) {
const prefix = "tufte";
// We attach a counter to the md instance or env to keep track of note IDs across rendering
let noteCounter = 0;
// Inline parsing rule for [^ ... ] and [^* ... ]
md.inline.ruler.after(
"image",
"tufte_notes",
(state: StateInline, silent: boolean): boolean => {
const max = state.posMax;
const start = state.pos;
// Check if it starts with '[^'
if (
start + 2 >= max || state.src.charCodeAt(start) !== 0x5B /* [ */ ||
state.src.charCodeAt(start + 1) !== 0x5E /* ^ */
) {
return false;
}
let pos = start + 2;
let isMarginNote = false;
// Check if it's a margin note (starts with '*')
if (pos < max && state.src.charCodeAt(pos) === 0x2A /* * */) {
isMarginNote = true;
pos++;
}
// Find the closing ']'
let level = 1;
let found = false;
const contentStart = pos;
while (pos < max) {
const code = state.src.charCodeAt(pos);
if (code === 0x5B /* [ */) {
level++;
} else if (code === 0x5D /* ] */) {
level--;
if (level === 0) {
found = true;
break;
}
}
pos++;
}
if (!found) return false;
// If we're just validating (silent mode), return true early
if (silent) return true;
const content = state.src.slice(contentStart, pos).trim();
noteCounter++;
const noteId = `${prefix}-${isMarginNote ? "mn" : "sn"}-${noteCounter}`;
// Update position in state past the closing ']'
state.pos = pos + 1;
// Create our custom open token
const token = state.push("tufte_note", "", 0);
token.meta = {
id: noteId,
isMarginNote,
content,
};
return true;
},
);
// Custom renderer for our token
md.renderer.rules.tufte_note = (
tokens: Token[],
idx: number,
_options: MarkdownItOptions,
env: any,
_self: Renderer,
) => {
const meta = tokens[idx].meta;
const { id, isMarginNote, content } = meta;
// Create or clone the environment object to communicate down-funnel
const nestedEnv = { ...env, tufte_nested: true };
// Render internal markdown styling (like bold, links, italics)
const renderedContent = md.renderInline(content, nestedEnv);
if (isMarginNote) {
// Margin note: Label only shows on mobile (via :not(.sidenote-number) CSS rule)
return `` +
`` +
`${renderedContent}`;
} else {
// Sidenote: Label shows on desktop and mobile to display the superscript number
return `` +
`` +
`${renderedContent}`;
}
};
}