@riebeckite/plugin-sidenotes 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +120 -0
- package/README_ja.md +124 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +81 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +504 -0
- package/dist/src/client.d.ts +17 -0
- package/dist/src/options.d.ts +10 -0
- package/dist/src/rehype.d.ts +2 -0
- package/dist/src/render.d.ts +49 -0
- package/dist/src/types.d.ts +68 -0
- package/package.json +61 -0
- package/style.css +127 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import {
|
|
3
|
+
createClientEntry,
|
|
4
|
+
createStyleAsset,
|
|
5
|
+
definePlugin
|
|
6
|
+
} from "@riebeckite/core";
|
|
7
|
+
|
|
8
|
+
// src/options.ts
|
|
9
|
+
var DEFAULT_SIDENOTES_ARIA_LABEL = "Sidenote";
|
|
10
|
+
var DEFAULT_SIDENOTES_OPEN_LABEL = "Footnote";
|
|
11
|
+
var DEFAULT_SIDENOTES_CLOSE_LABEL = "Close sidenote";
|
|
12
|
+
var DEFAULT_SIDENOTES_POPOVER_ALIGNMENT = "bottom";
|
|
13
|
+
function resolveSidenotesOptions(options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
className: normalizeNonEmpty(options.className, ""),
|
|
16
|
+
ariaLabel: normalizeNonEmpty(
|
|
17
|
+
options.ariaLabel,
|
|
18
|
+
DEFAULT_SIDENOTES_ARIA_LABEL
|
|
19
|
+
),
|
|
20
|
+
openLabel: normalizeNonEmpty(
|
|
21
|
+
options.openLabel,
|
|
22
|
+
DEFAULT_SIDENOTES_OPEN_LABEL
|
|
23
|
+
),
|
|
24
|
+
closeLabel: normalizeNonEmpty(
|
|
25
|
+
options.closeLabel,
|
|
26
|
+
DEFAULT_SIDENOTES_CLOSE_LABEL
|
|
27
|
+
),
|
|
28
|
+
popoverAlignment: normalizePopoverAlignment(
|
|
29
|
+
options.popoverAlignment,
|
|
30
|
+
DEFAULT_SIDENOTES_POPOVER_ALIGNMENT
|
|
31
|
+
)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalizeNonEmpty(value, fallback) {
|
|
35
|
+
if (typeof value !== "string") return fallback;
|
|
36
|
+
const trimmed = value.trim();
|
|
37
|
+
return trimmed === "" ? fallback : trimmed;
|
|
38
|
+
}
|
|
39
|
+
function normalizePopoverAlignment(value, fallback) {
|
|
40
|
+
return value === "end" ? "end" : value === "bottom" ? "bottom" : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/render.ts
|
|
44
|
+
import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
|
|
45
|
+
var SIDENOTES_TOGGLE_CLASS = "rr-sidenotes__toggle";
|
|
46
|
+
var SIDENOTES_NOTE_CLASS = "rr-sidenotes__note";
|
|
47
|
+
var SIDENOTES_POPOVER_PREFIX = "rr-sidenotes__note--popover-";
|
|
48
|
+
var SIDENOTES_INDEX_CLASS = "rr-sidenotes__index";
|
|
49
|
+
var SIDENOTES_BODY_CLASS = "rr-sidenotes__body";
|
|
50
|
+
var SIDENOTES_FOOTNOTES_CLASS = "rr-sidenotes__footnotes";
|
|
51
|
+
var SIDENOTES_NOTE_ID_PREFIX = "rr-sidenotes-";
|
|
52
|
+
var FOOTNOTE_HREF_PREFIX = "#user-content-fn-";
|
|
53
|
+
function sidenotesKey(href) {
|
|
54
|
+
if (href.startsWith(FOOTNOTE_HREF_PREFIX)) {
|
|
55
|
+
return href.slice(FOOTNOTE_HREF_PREFIX.length);
|
|
56
|
+
}
|
|
57
|
+
const marker = href.lastIndexOf("-fn-");
|
|
58
|
+
return marker === -1 ? null : href.slice(marker + "-fn-".length);
|
|
59
|
+
}
|
|
60
|
+
function sidenotesNoteId(key) {
|
|
61
|
+
return `${SIDENOTES_NOTE_ID_PREFIX}${key}`;
|
|
62
|
+
}
|
|
63
|
+
function renderSidenotesReference(input) {
|
|
64
|
+
const { href, id, index, options } = input;
|
|
65
|
+
const key = sidenotesKey(href) ?? "";
|
|
66
|
+
const label = `${options.openLabel} ${index}`.trim();
|
|
67
|
+
return `<a href="${escapeHtmlAttribute(href)}" id="${escapeHtmlAttribute(id)}" class="${SIDENOTES_TOGGLE_CLASS}" data-rr-sidenotes-ref aria-expanded="false" aria-controls="${escapeHtmlAttribute(sidenotesNoteId(key))}" aria-label="${escapeHtmlAttribute(label)}">${escapeHtml(index)}</a>`;
|
|
68
|
+
}
|
|
69
|
+
function renderSidenotesNote(input) {
|
|
70
|
+
const { key, index, bodyHtml, options } = input;
|
|
71
|
+
const className = [
|
|
72
|
+
SIDENOTES_NOTE_CLASS,
|
|
73
|
+
`${SIDENOTES_POPOVER_PREFIX}${options.popoverAlignment}`,
|
|
74
|
+
...options.className === "" ? [] : [options.className]
|
|
75
|
+
].join(" ");
|
|
76
|
+
const label = `${options.ariaLabel} ${index}`.trim();
|
|
77
|
+
return `<aside class="${escapeHtmlAttribute(className)}" id="${escapeHtmlAttribute(sidenotesNoteId(key))}" data-rr-sidenotes-note aria-label="${escapeHtmlAttribute(label)}" tabindex="-1"><span class="${SIDENOTES_INDEX_CLASS}" aria-hidden="true">${escapeHtml(index)}</span><div class="${SIDENOTES_BODY_CLASS}">${bodyHtml}</div></aside>`;
|
|
78
|
+
}
|
|
79
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
80
|
+
"area",
|
|
81
|
+
"base",
|
|
82
|
+
"br",
|
|
83
|
+
"col",
|
|
84
|
+
"embed",
|
|
85
|
+
"hr",
|
|
86
|
+
"img",
|
|
87
|
+
"input",
|
|
88
|
+
"link",
|
|
89
|
+
"meta",
|
|
90
|
+
"param",
|
|
91
|
+
"source",
|
|
92
|
+
"track",
|
|
93
|
+
"wbr"
|
|
94
|
+
]);
|
|
95
|
+
function renderHastChildren(children) {
|
|
96
|
+
return children.map(renderHastNode).join("");
|
|
97
|
+
}
|
|
98
|
+
function renderHastNode(node) {
|
|
99
|
+
if (node.type === "text" && typeof node.value === "string") {
|
|
100
|
+
return escapeHtml(node.value);
|
|
101
|
+
}
|
|
102
|
+
if (node.type === "raw" && typeof node.value === "string") {
|
|
103
|
+
return node.value;
|
|
104
|
+
}
|
|
105
|
+
if (node.type === "comment" && typeof node.value === "string") {
|
|
106
|
+
return `<!--${node.value}-->`;
|
|
107
|
+
}
|
|
108
|
+
if (node.type !== "element") return "";
|
|
109
|
+
const { tagName, properties, children = [] } = node;
|
|
110
|
+
const attributes = renderHastAttributes(properties);
|
|
111
|
+
const content = renderHastChildren(children);
|
|
112
|
+
if (VOID_ELEMENTS.has(tagName)) return `<${tagName}${attributes}>`;
|
|
113
|
+
return `<${tagName}${attributes}>${content}</${tagName}>`;
|
|
114
|
+
}
|
|
115
|
+
function renderHastAttributes(properties) {
|
|
116
|
+
if (!properties) return "";
|
|
117
|
+
let result = "";
|
|
118
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
119
|
+
if (value === null || value === void 0) continue;
|
|
120
|
+
const attributeName = name === "className" ? "class" : hyphenateAttribute(name);
|
|
121
|
+
result += ` ${attributeName}`;
|
|
122
|
+
if (value === false) continue;
|
|
123
|
+
if (value === true) {
|
|
124
|
+
result += '=""';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const stringValue = Array.isArray(value) ? value.filter((entry) => entry !== null && entry !== void 0).join(" ") : String(value);
|
|
128
|
+
result += `="${escapeHtmlAttribute(stringValue)}"`;
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function hyphenateAttribute(name) {
|
|
133
|
+
return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/rehype.ts
|
|
137
|
+
var FOOTNOTE_REFERENCE_ATTRIBUTE = "dataFootnoteRef";
|
|
138
|
+
var FOOTNOTES_SECTION_ATTRIBUTE = "dataFootnotes";
|
|
139
|
+
var FOOTNOTE_ID_PREFIX_PATTERN = /^(?:user-content-)?fn-(.+)$/;
|
|
140
|
+
var BLOCK_TAG_NAMES = /* @__PURE__ */ new Set([
|
|
141
|
+
"article",
|
|
142
|
+
"aside",
|
|
143
|
+
"blockquote",
|
|
144
|
+
"dd",
|
|
145
|
+
"div",
|
|
146
|
+
"dl",
|
|
147
|
+
"dt",
|
|
148
|
+
"figcaption",
|
|
149
|
+
"figure",
|
|
150
|
+
"footer",
|
|
151
|
+
"h1",
|
|
152
|
+
"h2",
|
|
153
|
+
"h3",
|
|
154
|
+
"h4",
|
|
155
|
+
"h5",
|
|
156
|
+
"h6",
|
|
157
|
+
"header",
|
|
158
|
+
"li",
|
|
159
|
+
"main",
|
|
160
|
+
"ol",
|
|
161
|
+
"p",
|
|
162
|
+
"pre",
|
|
163
|
+
"section",
|
|
164
|
+
"table",
|
|
165
|
+
"tbody",
|
|
166
|
+
"td",
|
|
167
|
+
"tfoot",
|
|
168
|
+
"th",
|
|
169
|
+
"thead",
|
|
170
|
+
"tr",
|
|
171
|
+
"ul"
|
|
172
|
+
]);
|
|
173
|
+
var SIDENOTES_REF_CLASS = "rr-sidenotes__ref";
|
|
174
|
+
function rehypeSidenotes(options = {}) {
|
|
175
|
+
const resolved = resolveSidenotesOptions(options);
|
|
176
|
+
return (tree) => {
|
|
177
|
+
transformSidenotes(tree, resolved);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function transformSidenotes(tree, options) {
|
|
181
|
+
const section = findFootnoteSection(tree);
|
|
182
|
+
if (!section) return;
|
|
183
|
+
const notes = collectFootnoteNotes(section);
|
|
184
|
+
if (notes.size === 0) return;
|
|
185
|
+
addClassName(section, SIDENOTES_FOOTNOTES_CLASS);
|
|
186
|
+
const insertions = [];
|
|
187
|
+
walkElements(tree, [], (ref, parent, index, ancestors) => {
|
|
188
|
+
if (!isFootnoteReference(ref)) return;
|
|
189
|
+
const href = getStringProperty(ref, "href");
|
|
190
|
+
if (!href) return;
|
|
191
|
+
const key = sidenotesKey(href);
|
|
192
|
+
if (!key) return;
|
|
193
|
+
const note = notes.get(key);
|
|
194
|
+
if (!note) return;
|
|
195
|
+
const noteIndex = getTextContent(ref);
|
|
196
|
+
const toggleHtml = renderSidenotesReference({
|
|
197
|
+
href,
|
|
198
|
+
id: getStringProperty(ref, "id") ?? "",
|
|
199
|
+
index: noteIndex,
|
|
200
|
+
options
|
|
201
|
+
});
|
|
202
|
+
if (parent.tagName === "sup") {
|
|
203
|
+
addClassName(parent, SIDENOTES_REF_CLASS);
|
|
204
|
+
(parent.children ?? []).splice(index, 1, rawNode(toggleHtml));
|
|
205
|
+
} else {
|
|
206
|
+
(parent.children ?? []).splice(
|
|
207
|
+
index,
|
|
208
|
+
1,
|
|
209
|
+
rawNode(`<sup class="${SIDENOTES_REF_CLASS}">${toggleHtml}</sup>`)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (note.used) return;
|
|
213
|
+
const { container, parent: containerParent } = findBlockContainer(
|
|
214
|
+
parent,
|
|
215
|
+
ancestors
|
|
216
|
+
);
|
|
217
|
+
if (!containerParent) return;
|
|
218
|
+
note.used = true;
|
|
219
|
+
note.index = noteIndex;
|
|
220
|
+
const noteHtml = renderSidenotesNote({
|
|
221
|
+
key,
|
|
222
|
+
index: noteIndex,
|
|
223
|
+
bodyHtml: note.bodyHtml,
|
|
224
|
+
options
|
|
225
|
+
});
|
|
226
|
+
let insertion = insertions.find((entry) => entry.container === container);
|
|
227
|
+
if (!insertion) {
|
|
228
|
+
insertion = { parent: containerParent, container, htmls: [] };
|
|
229
|
+
insertions.push(insertion);
|
|
230
|
+
}
|
|
231
|
+
insertion.htmls.push(noteHtml);
|
|
232
|
+
});
|
|
233
|
+
applyInsertions(insertions);
|
|
234
|
+
}
|
|
235
|
+
function findFootnoteSection(node) {
|
|
236
|
+
if (node.type === "element" && node.tagName === "section" && hasProperty(node, FOOTNOTES_SECTION_ATTRIBUTE)) {
|
|
237
|
+
return node;
|
|
238
|
+
}
|
|
239
|
+
for (const child of getChildren(node)) {
|
|
240
|
+
if (child.type !== "element") continue;
|
|
241
|
+
const found = findFootnoteSection(child);
|
|
242
|
+
if (found) return found;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function collectFootnoteNotes(section) {
|
|
247
|
+
const notes = /* @__PURE__ */ new Map();
|
|
248
|
+
const list = (section.children ?? []).find(
|
|
249
|
+
(child) => child.type === "element" && child.tagName === "ol"
|
|
250
|
+
);
|
|
251
|
+
if (!list) return notes;
|
|
252
|
+
for (const child of list.children ?? []) {
|
|
253
|
+
if (child.type !== "element" || child.tagName !== "li") continue;
|
|
254
|
+
const id = getStringProperty(child, "id") ?? "";
|
|
255
|
+
const match = FOOTNOTE_ID_PREFIX_PATTERN.exec(id);
|
|
256
|
+
if (!match) continue;
|
|
257
|
+
const key = match[1];
|
|
258
|
+
notes.set(key, {
|
|
259
|
+
key,
|
|
260
|
+
bodyHtml: renderHastChildren(
|
|
261
|
+
stripDefinitionContent(child.children ?? [])
|
|
262
|
+
),
|
|
263
|
+
index: "",
|
|
264
|
+
used: false
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return notes;
|
|
268
|
+
}
|
|
269
|
+
function stripDefinitionContent(children) {
|
|
270
|
+
const result = [];
|
|
271
|
+
for (const child of children) {
|
|
272
|
+
const stripped = stripDefinitionNode(child);
|
|
273
|
+
if (stripped) result.push(stripped);
|
|
274
|
+
}
|
|
275
|
+
const last = result[result.length - 1];
|
|
276
|
+
if (last?.type === "text") {
|
|
277
|
+
result[result.length - 1] = { type: "text", value: last.value.trimEnd() };
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
function stripDefinitionNode(node) {
|
|
282
|
+
if (node.type === "text") {
|
|
283
|
+
return node.value.trim() === "" ? null : node;
|
|
284
|
+
}
|
|
285
|
+
if (node.type !== "element") return node;
|
|
286
|
+
if (hasProperty(node, "dataFootnoteBackref")) return null;
|
|
287
|
+
return {
|
|
288
|
+
...node,
|
|
289
|
+
children: stripDefinitionContent(node.children ?? [])
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function walkElements(node, ancestors, onReference) {
|
|
293
|
+
if (node.type !== "element" && node.type !== "root") return;
|
|
294
|
+
const parent = node;
|
|
295
|
+
const children = parent.children ?? [];
|
|
296
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
297
|
+
const child = children[index];
|
|
298
|
+
if (child.type !== "element") continue;
|
|
299
|
+
if (isFootnoteReference(child)) {
|
|
300
|
+
onReference(child, parent, index, ancestors);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
walkElements(child, [parent, ...ancestors], onReference);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function findBlockContainer(parent, ancestors) {
|
|
307
|
+
const chain = [parent, ...ancestors];
|
|
308
|
+
for (let index = 0; index < chain.length; index += 1) {
|
|
309
|
+
if (BLOCK_TAG_NAMES.has(chain[index].tagName)) {
|
|
310
|
+
return {
|
|
311
|
+
container: chain[index],
|
|
312
|
+
parent: chain[index + 1] ?? null
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { container: chain[chain.length - 1], parent: null };
|
|
317
|
+
}
|
|
318
|
+
function applyInsertions(insertions) {
|
|
319
|
+
for (const insertion of insertions) {
|
|
320
|
+
const siblings = insertion.parent.children ?? [];
|
|
321
|
+
const index = siblings.indexOf(insertion.container);
|
|
322
|
+
if (index === -1) continue;
|
|
323
|
+
siblings.splice(index + 1, 0, ...insertion.htmls.map(rawNode));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function isFootnoteReference(node) {
|
|
327
|
+
return node.tagName === "a" && hasProperty(node, FOOTNOTE_REFERENCE_ATTRIBUTE);
|
|
328
|
+
}
|
|
329
|
+
function rawNode(value) {
|
|
330
|
+
return { type: "raw", value };
|
|
331
|
+
}
|
|
332
|
+
function addClassName(node, className) {
|
|
333
|
+
node.properties = {
|
|
334
|
+
...node.properties,
|
|
335
|
+
className: mergeClassName(node.properties?.className, className)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function mergeClassName(current, next) {
|
|
339
|
+
const currentClass = Array.isArray(current) ? current.join(" ") : typeof current === "string" ? current : "";
|
|
340
|
+
return currentClass === "" ? next : `${currentClass} ${next}`;
|
|
341
|
+
}
|
|
342
|
+
function getStringProperty(node, key) {
|
|
343
|
+
const value = getProperty(node, key);
|
|
344
|
+
if (typeof value === "string") return value;
|
|
345
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function hasProperty(node, key) {
|
|
349
|
+
return getProperty(node, key) !== void 0;
|
|
350
|
+
}
|
|
351
|
+
function getProperty(node, key) {
|
|
352
|
+
const value = node.properties?.[key];
|
|
353
|
+
if (value !== void 0) return value;
|
|
354
|
+
const hyphenated = key.replace(
|
|
355
|
+
/[A-Z]/g,
|
|
356
|
+
(letter) => `-${letter.toLowerCase()}`
|
|
357
|
+
);
|
|
358
|
+
if (hyphenated !== key) return node.properties?.[hyphenated];
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
function getTextContent(node) {
|
|
362
|
+
if (node.type === "text" && typeof node.value === "string") {
|
|
363
|
+
return node.value;
|
|
364
|
+
}
|
|
365
|
+
return getChildren(node).map(getTextContent).join("");
|
|
366
|
+
}
|
|
367
|
+
function getChildren(node) {
|
|
368
|
+
const children = node.children;
|
|
369
|
+
return Array.isArray(children) ? children : [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/client.ts
|
|
373
|
+
var DEFAULT_SELECTOR = "[data-rr-sidenotes-ref]";
|
|
374
|
+
var NOTE_OPEN_CLASS = "rr-sidenotes__note--open";
|
|
375
|
+
var DESKTOP_QUERY = "(min-width: 48rem)";
|
|
376
|
+
var DEFAULT_OPEN_LABEL = "Footnote";
|
|
377
|
+
var DEFAULT_CLOSE_LABEL = "Close sidenote";
|
|
378
|
+
function initSidenotes(options = {}) {
|
|
379
|
+
const desktopQuery = window.matchMedia(DESKTOP_QUERY);
|
|
380
|
+
if (desktopQuery.matches) return;
|
|
381
|
+
const toggles = Array.from(
|
|
382
|
+
document.querySelectorAll(
|
|
383
|
+
options.selector ?? DEFAULT_SELECTOR
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
if (toggles.length === 0) return;
|
|
387
|
+
const openLabel = options.openLabel ?? DEFAULT_OPEN_LABEL;
|
|
388
|
+
const closeLabel = options.closeLabel ?? DEFAULT_CLOSE_LABEL;
|
|
389
|
+
const noteFor = (toggle) => {
|
|
390
|
+
const controls = toggle.getAttribute("aria-controls");
|
|
391
|
+
if (!controls) return null;
|
|
392
|
+
const note = document.getElementById(controls);
|
|
393
|
+
return note instanceof HTMLElement ? note : null;
|
|
394
|
+
};
|
|
395
|
+
const isInsideSidenotes = (target) => {
|
|
396
|
+
if (!target) return false;
|
|
397
|
+
for (const toggle of toggles) {
|
|
398
|
+
const note = noteFor(toggle);
|
|
399
|
+
if (toggle.contains(target) || note?.contains(target)) return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
};
|
|
403
|
+
const setOpen = (toggle, note, open) => {
|
|
404
|
+
const index = toggle.textContent?.trim() ?? "";
|
|
405
|
+
const label = open ? `${closeLabel} ${index}`.trim() : `${openLabel} ${index}`.trim();
|
|
406
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
407
|
+
toggle.setAttribute("aria-label", label);
|
|
408
|
+
note.classList.toggle(NOTE_OPEN_CLASS, open);
|
|
409
|
+
if (open) note.focus({ preventScroll: true });
|
|
410
|
+
};
|
|
411
|
+
const closeAll = () => {
|
|
412
|
+
for (const toggle of toggles) {
|
|
413
|
+
const note = noteFor(toggle);
|
|
414
|
+
if (note?.classList.contains(NOTE_OPEN_CLASS)) {
|
|
415
|
+
setOpen(toggle, note, false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
for (const toggle of toggles) {
|
|
420
|
+
toggle.addEventListener("click", (event) => {
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
const note = noteFor(toggle);
|
|
423
|
+
if (!note) return;
|
|
424
|
+
const isOpen = note.classList.contains(NOTE_OPEN_CLASS);
|
|
425
|
+
if (!isOpen) closeAll();
|
|
426
|
+
setOpen(toggle, note, !isOpen);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const onKeydown = (event) => {
|
|
430
|
+
if (event.key !== "Escape") return;
|
|
431
|
+
closeAll();
|
|
432
|
+
};
|
|
433
|
+
const onClickOutside = (event) => {
|
|
434
|
+
if (isInsideSidenotes(event.target instanceof Node ? event.target : null)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
closeAll();
|
|
438
|
+
};
|
|
439
|
+
document.addEventListener("keydown", onKeydown);
|
|
440
|
+
document.addEventListener("click", onClickOutside);
|
|
441
|
+
const onViewportChange = (event) => {
|
|
442
|
+
if (!event.matches) return;
|
|
443
|
+
closeAll();
|
|
444
|
+
document.removeEventListener("keydown", onKeydown);
|
|
445
|
+
document.removeEventListener("click", onClickOutside);
|
|
446
|
+
desktopQuery.removeEventListener("change", onViewportChange);
|
|
447
|
+
};
|
|
448
|
+
desktopQuery.addEventListener("change", onViewportChange);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// index.ts
|
|
452
|
+
var SIDENOTES_PLUGIN_NAME = "sidenotes";
|
|
453
|
+
function sidenotes(options = {}) {
|
|
454
|
+
const resolved = resolveSidenotesOptions(options);
|
|
455
|
+
return definePlugin({
|
|
456
|
+
name: SIDENOTES_PLUGIN_NAME,
|
|
457
|
+
options,
|
|
458
|
+
validateOptions: validateSidenotesOptions,
|
|
459
|
+
rehypePlugins: [() => rehypeSidenotes(resolved)],
|
|
460
|
+
assets: [createStyleAsset(SIDENOTES_PLUGIN_NAME)],
|
|
461
|
+
clientEntries: [
|
|
462
|
+
createClientEntry(SIDENOTES_PLUGIN_NAME, "initSidenotes", {
|
|
463
|
+
openLabel: resolved.openLabel,
|
|
464
|
+
closeLabel: resolved.closeLabel
|
|
465
|
+
})
|
|
466
|
+
]
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
var sidenotesPlugin = sidenotes;
|
|
470
|
+
function validateSidenotesOptions(options) {
|
|
471
|
+
if (!options) return [];
|
|
472
|
+
const issues = [];
|
|
473
|
+
for (const key of [
|
|
474
|
+
"className",
|
|
475
|
+
"ariaLabel",
|
|
476
|
+
"openLabel",
|
|
477
|
+
"closeLabel"
|
|
478
|
+
]) {
|
|
479
|
+
const value = options[key];
|
|
480
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) {
|
|
481
|
+
issues.push({ path: key, message: "Expected a non-empty string." });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (options.popoverAlignment !== void 0 && options.popoverAlignment !== "bottom" && options.popoverAlignment !== "end") {
|
|
485
|
+
issues.push({
|
|
486
|
+
path: "popoverAlignment",
|
|
487
|
+
message: 'Expected "bottom" or "end".'
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return issues;
|
|
491
|
+
}
|
|
492
|
+
export {
|
|
493
|
+
SIDENOTES_PLUGIN_NAME,
|
|
494
|
+
initSidenotes,
|
|
495
|
+
rehypeSidenotes,
|
|
496
|
+
renderHastChildren,
|
|
497
|
+
renderSidenotesNote,
|
|
498
|
+
renderSidenotesReference,
|
|
499
|
+
resolveSidenotesOptions,
|
|
500
|
+
sidenotes,
|
|
501
|
+
sidenotesKey,
|
|
502
|
+
sidenotesNoteId,
|
|
503
|
+
sidenotesPlugin
|
|
504
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile popover behavior for `@riebeckite/plugin-sidenotes`.
|
|
3
|
+
*
|
|
4
|
+
* Desktop margin notes are static and need no JavaScript: this entry ignores
|
|
5
|
+
* the page whenever `(min-width: 48rem)` matches and disconnects itself when
|
|
6
|
+
* the viewport crosses into the desktop layout.
|
|
7
|
+
*/
|
|
8
|
+
/** Public options exposed to the browser through the plugin's client entry. */
|
|
9
|
+
export type SidenotesClientOptions = {
|
|
10
|
+
/** Selector for reference links. Defaults to `[data-rr-sidenotes-ref]`. */
|
|
11
|
+
selector?: string;
|
|
12
|
+
/** Accessible label prefix for a reference link while its popover is closed. */
|
|
13
|
+
openLabel?: string;
|
|
14
|
+
/** Accessible label prefix for a reference link while its popover is open. */
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function initSidenotes(options?: SidenotesClientOptions): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResolvedSidenotesOptions, SidenotesOptions } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_SIDENOTES_ARIA_LABEL = "Sidenote";
|
|
3
|
+
export declare const DEFAULT_SIDENOTES_OPEN_LABEL = "Footnote";
|
|
4
|
+
export declare const DEFAULT_SIDENOTES_CLOSE_LABEL = "Close sidenote";
|
|
5
|
+
export declare const DEFAULT_SIDENOTES_POPOVER_ALIGNMENT = "bottom";
|
|
6
|
+
/**
|
|
7
|
+
* Applies defaults to `SidenotesOptions`. Pure and deterministic so the
|
|
8
|
+
* plugin can resolve options once and reuse them for every processed tree.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSidenotesOptions(options?: SidenotesOptions): ResolvedSidenotesOptions;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HastNode, ResolvedSidenotesOptions } from "./types.js";
|
|
2
|
+
/** Feature hooks follow the `rr-sidenotes` convention from the CSS hooks doc. */
|
|
3
|
+
export declare const SIDENOTES_REF_CLASS = "rr-sidenotes__ref";
|
|
4
|
+
export declare const SIDENOTES_TOGGLE_CLASS = "rr-sidenotes__toggle";
|
|
5
|
+
export declare const SIDENOTES_NOTE_CLASS = "rr-sidenotes__note";
|
|
6
|
+
export declare const SIDENOTES_NOTE_OPEN_MODIFIER = "rr-sidenotes__note--open";
|
|
7
|
+
export declare const SIDENOTES_POPOVER_PREFIX = "rr-sidenotes__note--popover-";
|
|
8
|
+
export declare const SIDENOTES_INDEX_CLASS = "rr-sidenotes__index";
|
|
9
|
+
export declare const SIDENOTES_BODY_CLASS = "rr-sidenotes__body";
|
|
10
|
+
export declare const SIDENOTES_FOOTNOTES_CLASS = "rr-sidenotes__footnotes";
|
|
11
|
+
export declare const SIDENOTES_NOTE_ID_PREFIX = "rr-sidenotes-";
|
|
12
|
+
/** Extracts the footnote key from a reference href. Returns `null` when the
|
|
13
|
+
* href does not look like a footnote target. */
|
|
14
|
+
export declare function sidenotesKey(href: string): string | null;
|
|
15
|
+
/** Stable id shared by a sidenote and its toggles' `aria-controls`. */
|
|
16
|
+
export declare function sidenotesNoteId(key: string): string;
|
|
17
|
+
export type SidenotesReferenceInput = {
|
|
18
|
+
/** Existing `href` of the GFM footnote reference, kept for reachability. */
|
|
19
|
+
href: string;
|
|
20
|
+
/** Existing `id` of the GFM footnote reference, kept for back-links. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Display number already chosen by the footnote pipeline. */
|
|
23
|
+
index: string;
|
|
24
|
+
options: ResolvedSidenotesOptions;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Renders the inline reference link that replaces a GFM footnote reference.
|
|
28
|
+
* The surrounding `sup` is managed by the rehype transformer so it can reuse
|
|
29
|
+
* the existing wrapper when one is already present.
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderSidenotesReference(input: SidenotesReferenceInput): string;
|
|
32
|
+
export type SidenotesNoteInput = {
|
|
33
|
+
key: string;
|
|
34
|
+
index: string;
|
|
35
|
+
/** Serialized definition content (back-links stripped). */
|
|
36
|
+
bodyHtml: string;
|
|
37
|
+
options: ResolvedSidenotesOptions;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Renders the sidenote: a static margin note on desktop and the popover
|
|
41
|
+
* source on mobile.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderSidenotesNote(input: SidenotesNoteInput): string;
|
|
44
|
+
/**
|
|
45
|
+
* Serializes existing hast children into the HTML embedded in a sidenote
|
|
46
|
+
* body. The output is inserted as a `raw` node and serialized verbatim, so
|
|
47
|
+
* every text value and attribute is escaped here.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderHastChildren(children: readonly HastNode[]): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Options accepted by `sidenotes()`. */
|
|
2
|
+
export type SidenotesOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Extra CSS class appended to each sidenote root, on top of
|
|
5
|
+
* `rr-sidenotes__note` and the popover modifier. Defaults to none.
|
|
6
|
+
*/
|
|
7
|
+
className?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Accessible name prefix for each sidenote. The rendered label is
|
|
10
|
+
* `"${ariaLabel} ${index}"`. Defaults to `"Sidenote"`.
|
|
11
|
+
*/
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Accessible label prefix for a reference link while its mobile popover is
|
|
15
|
+
* closed. The rendered label is `"${openLabel} ${index}"`. Defaults to
|
|
16
|
+
* `"Footnote"`.
|
|
17
|
+
*/
|
|
18
|
+
openLabel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Accessible label prefix for a reference link while its mobile popover is
|
|
21
|
+
* open. The rendered label is `"${closeLabel} ${index}"`. Defaults to
|
|
22
|
+
* `"Close sidenote"`.
|
|
23
|
+
*/
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Where the mobile popover is anchored. Defaults to `"bottom"` (a bottom
|
|
27
|
+
* sheet); `"end"` is a right-edge panel.
|
|
28
|
+
*/
|
|
29
|
+
popoverAlignment?: "bottom" | "end";
|
|
30
|
+
};
|
|
31
|
+
/** `SidenotesOptions` with every default applied. */
|
|
32
|
+
export type ResolvedSidenotesOptions = {
|
|
33
|
+
className: string;
|
|
34
|
+
ariaLabel: string;
|
|
35
|
+
openLabel: string;
|
|
36
|
+
closeLabel: string;
|
|
37
|
+
popoverAlignment: "bottom" | "end";
|
|
38
|
+
};
|
|
39
|
+
/** Minimal hast element shape the sidenotes transformer reads and emits. */
|
|
40
|
+
export type HastElementNode = {
|
|
41
|
+
type: "element";
|
|
42
|
+
tagName: string;
|
|
43
|
+
properties?: Record<string, unknown>;
|
|
44
|
+
children?: HastNode[];
|
|
45
|
+
};
|
|
46
|
+
export type HastTextNode = {
|
|
47
|
+
type: "text";
|
|
48
|
+
value: string;
|
|
49
|
+
};
|
|
50
|
+
/** A raw HTML fragment emitted by the plugin and serialized verbatim. */
|
|
51
|
+
export type HastRawNode = {
|
|
52
|
+
type: "raw";
|
|
53
|
+
value: string;
|
|
54
|
+
};
|
|
55
|
+
export type HastCommentNode = {
|
|
56
|
+
type: "comment";
|
|
57
|
+
value: string;
|
|
58
|
+
};
|
|
59
|
+
export type HastDoctypeNode = {
|
|
60
|
+
type: "doctype";
|
|
61
|
+
name?: string;
|
|
62
|
+
};
|
|
63
|
+
export type HastRootNode = {
|
|
64
|
+
type: "root";
|
|
65
|
+
children?: HastNode[];
|
|
66
|
+
};
|
|
67
|
+
/** Any node the sidenotes transformer may encounter in a hast tree. */
|
|
68
|
+
export type HastNode = HastElementNode | HastTextNode | HastRawNode | HastCommentNode | HastDoctypeNode | HastRootNode;
|