@riebeckite/plugin-citations 0.0.21 → 0.0.23
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/README.md +45 -0
- package/README_ja.md +31 -0
- package/dist/index.js +282 -56
- package/dist/src/page.d.ts +11 -0
- package/dist/src/paths.d.ts +12 -0
- package/dist/src/render.d.ts +6 -0
- package/dist/src/types.d.ts +1 -0
- package/package.json +9 -5
- package/style.css +28 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @riebeckite/plugin-citations
|
|
2
2
|
|
|
3
|
+
<!-- Generated from docs/docs/plugins/citations.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
|
|
4
|
+
|
|
3
5
|
[日本語版](./README_ja.md)
|
|
4
6
|
|
|
5
7
|
Official Riebeckite plugin for BibTeX / BibLaTeX based citations in Markdown and Obsidian notes.
|
|
@@ -61,6 +63,49 @@ Use `referencesHeading` for localized headings:
|
|
|
61
63
|
citations({ bibliography: "references.bib", referencesHeading: "参考文献" })
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
## Bibliography page
|
|
67
|
+
|
|
68
|
+
Setting `pagePath` generates a standalone bibliography page. It is opt-in and
|
|
69
|
+
disabled when omitted:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
citations({ bibliography: "references.bib", pagePath: "/bibliography" })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The page type (`citations-bibliography`) collects references from two sources:
|
|
76
|
+
|
|
77
|
+
- the plugin's `bibliography` option, resolved from the content root
|
|
78
|
+
- the frontmatter `bibliography` of every discoverable (public) page
|
|
79
|
+
|
|
80
|
+
Only `manifest.discoverableEntries` contribute frontmatter bibliographies. Pages
|
|
81
|
+
that are draft, unlisted, scheduled, or otherwise non-discoverable are ignored,
|
|
82
|
+
so their bibliographies are never leaked into the page.
|
|
83
|
+
|
|
84
|
+
Entries from all files are merged in a deterministic order. Identical duplicates
|
|
85
|
+
are dropped silently; when the same key appears with different content, the first
|
|
86
|
+
definition wins and the conflict is reported as `citation-duplicate-key`. Missing
|
|
87
|
+
files and malformed files are reported as `citation-missing-bibliography` and
|
|
88
|
+
`citation-malformed-bibliography`. When no entry is collected, no page is
|
|
89
|
+
generated. If a Markdown page or redirect already owns the path, Markdown wins
|
|
90
|
+
and the page is skipped with a `citations-page-conflict` diagnostic.
|
|
91
|
+
|
|
92
|
+
The page body provides its own `<main>` and `<h1>`, escapes all entry text, and
|
|
93
|
+
lists entries sorted by key. A `url` or `doi` field becomes a link only when it
|
|
94
|
+
resolves to a safe `http(s)` target. Existing in-page citations and the
|
|
95
|
+
`References` section are unchanged.
|
|
96
|
+
|
|
97
|
+
Bibliography pages declare a `global` output dependency, so changes to a
|
|
98
|
+
bibliography file, a frontmatter `bibliography`, page visibility, or `pagePath`
|
|
99
|
+
invalidate the affected output.
|
|
100
|
+
|
|
101
|
+
## Options
|
|
102
|
+
|
|
103
|
+
| Option | Default | Description |
|
|
104
|
+
| ------ | ------- | ----------- |
|
|
105
|
+
| `bibliography` | unset | Vault-relative BibTeX/BibLaTeX file path or paths |
|
|
106
|
+
| `referencesHeading` | `"References"` | Heading inserted above generated references |
|
|
107
|
+
| `pagePath` | unset | Path of the standalone bibliography page; disabled when omitted |
|
|
108
|
+
|
|
64
109
|
## Diagnostics
|
|
65
110
|
|
|
66
111
|
Reported through Riebeckite diagnostics:
|
package/README_ja.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @riebeckite/plugin-citations
|
|
2
2
|
|
|
3
|
+
<!-- Generated from docs/docs/plugins/citations.ja.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
|
|
4
|
+
|
|
3
5
|
[English](./README.md)
|
|
4
6
|
|
|
5
7
|
Markdown / Obsidian ノートで BibTeX / BibLaTeX の文献情報を使って引用を表示する、Riebeckite 公式プラグインです。
|
|
@@ -61,6 +63,35 @@ Riebeckite はこのプラグインより先に `:name` を directive として
|
|
|
61
63
|
citations({ bibliography: "references.bib", referencesHeading: "参考文献" })
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
## 参考文献ページ
|
|
67
|
+
|
|
68
|
+
`pagePath` を設定すると、参考文献の一覧ページを生成します。既定では無効で、指定しないかぎり生成しません。
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
citations({ bibliography: "references.bib", pagePath: "/bibliography" })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
ページタイプ(`citations-bibliography`)は、次の2か所から文献を集めます。
|
|
75
|
+
|
|
76
|
+
- プラグインの `bibliography` オプション(コンテンツルートからの相対パス)
|
|
77
|
+
- 公開(discoverable)ページの frontmatter `bibliography`
|
|
78
|
+
|
|
79
|
+
frontmatter の `bibliography` を取り込むのは `manifest.discoverableEntries` だけです。下書き・unlisted・公開予定など、discoverable でないページは無視するため、それらの文献がページへ漏れることはありません。
|
|
80
|
+
|
|
81
|
+
各ファイルの文献は決定的な順序で統合します。内容まで同じ重複は黙って1件にまとめ、同じキーで内容が異なる場合は先に見つかった定義を採用し、`citation-duplicate-key` として報告します。ファイルが見つからない場合は `citation-missing-bibliography`、解析できない場合は `citation-malformed-bibliography` を報告します。文献が1件も集まらないときはページを生成しません。同じパスを Markdown ページやリダイレクトが既に持っている場合は Markdown を優先し、`citations-page-conflict` を報告してページを生成しません。
|
|
82
|
+
|
|
83
|
+
ページ本文には専用の `<main>` と `<h1>` を持たせ、文献のテキストはすべてエスケープします。`url` と `doi` は安全な `http(s)` のときだけリンクにします。文献はキー順に並べます。既存の本文中の引用と `References` セクションは変更しません。
|
|
84
|
+
|
|
85
|
+
参考文献ページは `global` の output dependency を宣言します。文献ファイル、frontmatter の `bibliography`、ページの公開状態、`pagePath` の変更で、影響する出力を無効化します。
|
|
86
|
+
|
|
87
|
+
## オプション
|
|
88
|
+
|
|
89
|
+
| 項目 | 既定値 | 説明 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `bibliography` | 未設定 | コンテンツルートからの相対パスで指定する BibTeX / BibLaTeX ファイル |
|
|
92
|
+
| `referencesHeading` | `"References"` | 生成する参考文献の見出し |
|
|
93
|
+
| `pagePath` | 未設定 | 参考文献一覧ページのパス。指定しないときは生成しない |
|
|
94
|
+
|
|
64
95
|
## diagnostics
|
|
65
96
|
|
|
66
97
|
以下を Riebeckite の diagnostics に出力します。
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// index.ts
|
|
2
2
|
import {
|
|
3
|
+
createStyleAsset,
|
|
3
4
|
definePlugin,
|
|
4
|
-
readContentSourceEntry
|
|
5
|
+
readContentSourceEntry as readContentSourceEntry2
|
|
5
6
|
} from "@riebeckite/core";
|
|
6
7
|
|
|
7
|
-
// src/
|
|
8
|
-
import {
|
|
8
|
+
// src/page.ts
|
|
9
|
+
import {
|
|
10
|
+
readContentSourceEntry
|
|
11
|
+
} from "@riebeckite/core";
|
|
9
12
|
|
|
10
13
|
// src/bibliography.ts
|
|
11
14
|
var SUPPORTED_TYPES = /* @__PURE__ */ new Set(["article", "book", "inproceedings", "misc"]);
|
|
@@ -284,7 +287,245 @@ function cleanValue(value) {
|
|
|
284
287
|
return value.replace(/[{}]/g, "").replace(/\\([&%$#_{}])/g, "$1").trim();
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
// src/paths.ts
|
|
291
|
+
function pathList(value) {
|
|
292
|
+
if (typeof value === "string") return [value];
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
return value.filter((item) => typeof item === "string");
|
|
295
|
+
}
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
function pageRelativeCandidates(path, sourceSlug) {
|
|
299
|
+
const normalized = normalizeLogicalPath(path);
|
|
300
|
+
if (!sourceSlug || isAbsoluteLikePath(normalized)) return [normalized];
|
|
301
|
+
const directory = normalizeLogicalPath(sourceSlug).split("/").slice(0, -1).join("/");
|
|
302
|
+
if (!directory) return [normalized];
|
|
303
|
+
const relative = normalizeLogicalPath(`${directory}/${normalized}`);
|
|
304
|
+
return relative === normalized ? [normalized] : [relative, normalized];
|
|
305
|
+
}
|
|
306
|
+
function isAbsoluteLikePath(path) {
|
|
307
|
+
return path.startsWith("/") || /^[A-Za-z]:\//.test(path);
|
|
308
|
+
}
|
|
309
|
+
function normalizeLogicalPath(path) {
|
|
310
|
+
const parts = [];
|
|
311
|
+
for (const segment of path.replace(/\\/g, "/").split("/")) {
|
|
312
|
+
if (!segment || segment === ".") continue;
|
|
313
|
+
if (segment === "..") {
|
|
314
|
+
if (parts.length > 0 && parts.at(-1) !== "..") parts.pop();
|
|
315
|
+
else parts.push(segment);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
parts.push(segment);
|
|
319
|
+
}
|
|
320
|
+
const prefix = path.startsWith("/") ? "/" : "";
|
|
321
|
+
return `${prefix}${parts.join("/")}`;
|
|
322
|
+
}
|
|
323
|
+
function pushUniqueDiagnostics(target, diagnostics, slug) {
|
|
324
|
+
for (const diagnostic of diagnostics) {
|
|
325
|
+
const next = diagnostic.slug ? diagnostic : { ...diagnostic, slug };
|
|
326
|
+
const signature = diagnosticSignature(next);
|
|
327
|
+
if (target.some((item) => diagnosticSignature(item) === signature))
|
|
328
|
+
continue;
|
|
329
|
+
target.push(next);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function diagnosticSignature(diagnostic) {
|
|
333
|
+
return `${diagnostic.code}:${diagnostic.slug ?? ""}:${diagnostic.filePath ?? ""}:${diagnostic.target ?? ""}:${diagnostic.message}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/render.ts
|
|
337
|
+
import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
|
|
338
|
+
function renderBibliographyPage(input) {
|
|
339
|
+
const items = input.entries.map(([key, entry]) => renderItem(key, entry)).join("");
|
|
340
|
+
return `<main class="rb-citations-page"><h1 class="rb-citations-page__title">${escapeHtml(
|
|
341
|
+
input.title
|
|
342
|
+
)}</h1><ol class="rb-citations-page__list">${items}</ol></main>`;
|
|
343
|
+
}
|
|
344
|
+
function renderItem(key, entry) {
|
|
345
|
+
const fields = entry.fields;
|
|
346
|
+
const author = fields.author ?? fields.editor;
|
|
347
|
+
const year = fields.year ?? fields.date;
|
|
348
|
+
const container = fields.journal ?? fields.booktitle ?? fields.publisher;
|
|
349
|
+
const parts = [];
|
|
350
|
+
if (author)
|
|
351
|
+
parts.push(
|
|
352
|
+
`<span class="rb-citations-page__author">${escapeHtml(author)}</span>`
|
|
353
|
+
);
|
|
354
|
+
if (year)
|
|
355
|
+
parts.push(
|
|
356
|
+
`<span class="rb-citations-page__year">${escapeHtml(year)}</span>`
|
|
357
|
+
);
|
|
358
|
+
if (fields.title)
|
|
359
|
+
parts.push(
|
|
360
|
+
`<span class="rb-citations-page__work">${escapeHtml(fields.title)}</span>`
|
|
361
|
+
);
|
|
362
|
+
if (container)
|
|
363
|
+
parts.push(
|
|
364
|
+
`<span class="rb-citations-page__container">${escapeHtml(container)}</span>`
|
|
365
|
+
);
|
|
366
|
+
if (parts.length === 0)
|
|
367
|
+
parts.push(
|
|
368
|
+
`<span class="rb-citations-page__key">${escapeHtml(key)}</span>`
|
|
369
|
+
);
|
|
370
|
+
const id = escapeHtmlAttribute(referenceId(key));
|
|
371
|
+
return `<li class="rb-citations-page__item" id="${id}">${parts.join(
|
|
372
|
+
" "
|
|
373
|
+
)}${renderLink(fields)}</li>`;
|
|
374
|
+
}
|
|
375
|
+
function renderLink(fields) {
|
|
376
|
+
const href = safeExternalUrl(fields.url) ?? safeDoiUrl(fields.doi);
|
|
377
|
+
if (!href) return "";
|
|
378
|
+
return ` <a class="rb-citations-page__link" href="${escapeHtmlAttribute(
|
|
379
|
+
href
|
|
380
|
+
)}" rel="noopener noreferrer">${escapeHtml(href)}</a>`;
|
|
381
|
+
}
|
|
382
|
+
function safeExternalUrl(value) {
|
|
383
|
+
if (!value) return null;
|
|
384
|
+
try {
|
|
385
|
+
const parsed = new URL(value);
|
|
386
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
387
|
+
return null;
|
|
388
|
+
return parsed.toString();
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function safeDoiUrl(value) {
|
|
394
|
+
if (!value) return null;
|
|
395
|
+
const doi = value.trim().replace(/^https?:\/\/(?:dx\.)?doi\.org\//i, "").replace(/^doi:\s*/i, "");
|
|
396
|
+
if (!/^10\.\d{4,9}\//.test(doi)) return null;
|
|
397
|
+
return `https://doi.org/${encodeURI(doi)}`;
|
|
398
|
+
}
|
|
399
|
+
function referenceId(key) {
|
|
400
|
+
const sanitized = key.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
401
|
+
return `ref-${sanitized || "key"}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/page.ts
|
|
405
|
+
var PLUGIN_NAME = "citations";
|
|
406
|
+
var PAGE_TITLE = "Bibliography";
|
|
407
|
+
var EXTENSION = /\.[a-zA-Z0-9]+$/;
|
|
408
|
+
async function listBibliographyPagePaths(source, options) {
|
|
409
|
+
const pagePath = bibliographyPagePath(options.pagePath);
|
|
410
|
+
if (!pagePath) return [];
|
|
411
|
+
if (isContentRoute(source.manifest, pagePath)) {
|
|
412
|
+
source.diagnostics.push({
|
|
413
|
+
code: "citations-page-conflict",
|
|
414
|
+
severity: "warning",
|
|
415
|
+
pluginName: PLUGIN_NAME,
|
|
416
|
+
message: `Skipping bibliography page ${pagePath}: a content route already owns it.`,
|
|
417
|
+
target: pagePath
|
|
418
|
+
});
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const entries = await collectBibliographyEntries(source, options);
|
|
422
|
+
return entries.size > 0 ? [pagePath] : [];
|
|
423
|
+
}
|
|
424
|
+
async function resolveBibliographyPage(context, options) {
|
|
425
|
+
const pagePath = bibliographyPagePath(options.pagePath);
|
|
426
|
+
if (!pagePath || pagePath !== context.pathname) return null;
|
|
427
|
+
if (isContentRoute(context.manifest, context.pathname)) return null;
|
|
428
|
+
const entries = await collectBibliographyEntries(context, options);
|
|
429
|
+
if (entries.size === 0) return null;
|
|
430
|
+
const sorted = [...entries.entries()].toSorted(
|
|
431
|
+
(a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
|
|
432
|
+
);
|
|
433
|
+
return {
|
|
434
|
+
type: "citations-bibliography",
|
|
435
|
+
pathname: context.pathname,
|
|
436
|
+
title: PAGE_TITLE,
|
|
437
|
+
body: renderBibliographyPage({ title: PAGE_TITLE, entries: sorted })
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function bibliographyPagePath(value) {
|
|
441
|
+
if (typeof value !== "string") return null;
|
|
442
|
+
const segments = normalizeLogicalPath(value).replace(/^\/+/, "").split("/").filter(Boolean);
|
|
443
|
+
if (segments.length === 0) return null;
|
|
444
|
+
if (EXTENSION.test(segments[segments.length - 1] ?? "")) return null;
|
|
445
|
+
return `/${segments.join("/")}`;
|
|
446
|
+
}
|
|
447
|
+
function isContentRoute(manifest, pathname) {
|
|
448
|
+
return manifest.byRoutablePermalink.has(pathname) || manifest.publicRedirects.has(pathname);
|
|
449
|
+
}
|
|
450
|
+
function collectBibliographyGroups(manifest, options) {
|
|
451
|
+
const groups = [];
|
|
452
|
+
for (const path of pathList(options.bibliography)) {
|
|
453
|
+
groups.push([path]);
|
|
454
|
+
}
|
|
455
|
+
const frontmatterEntries = manifest.discoverableEntries.filter((entry) => entry.frontmatter.bibliography !== void 0).toSorted((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
456
|
+
for (const entry of frontmatterEntries) {
|
|
457
|
+
for (const path of pathList(entry.frontmatter.bibliography)) {
|
|
458
|
+
groups.push([...pageRelativeCandidates(path, entry.slug)]);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return groups;
|
|
462
|
+
}
|
|
463
|
+
async function collectBibliographyEntries(source, options) {
|
|
464
|
+
const groups = collectBibliographyGroups(source.manifest, options);
|
|
465
|
+
const entries = /* @__PURE__ */ new Map();
|
|
466
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
467
|
+
const collected = [];
|
|
468
|
+
for (const candidates of groups) {
|
|
469
|
+
const resolved = await loadFirstAvailable(source.contentSource, candidates);
|
|
470
|
+
if (!resolved) {
|
|
471
|
+
collected.push({
|
|
472
|
+
code: "citation-missing-bibliography",
|
|
473
|
+
severity: "error",
|
|
474
|
+
message: `Bibliography file was not found: ${candidates[0] ?? ""}`,
|
|
475
|
+
filePath: candidates[0]
|
|
476
|
+
});
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (loaded.has(resolved.path)) continue;
|
|
480
|
+
loaded.add(resolved.path);
|
|
481
|
+
collected.push(...resolved.parsed.diagnostics);
|
|
482
|
+
mergeEntries(entries, resolved.parsed.entries, resolved.path, collected);
|
|
483
|
+
}
|
|
484
|
+
pushUniqueDiagnostics(source.diagnostics, collected, void 0);
|
|
485
|
+
return entries;
|
|
486
|
+
}
|
|
487
|
+
async function loadFirstAvailable(source, candidates) {
|
|
488
|
+
if (!source) return null;
|
|
489
|
+
for (const path of candidates) {
|
|
490
|
+
const raw = await readContentSourceEntry(source, path);
|
|
491
|
+
if (raw === null) continue;
|
|
492
|
+
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
493
|
+
return { path, parsed: parseBibliography(text, path) };
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function mergeEntries(target, incoming, filePath, diagnostics) {
|
|
498
|
+
for (const [key, entry] of incoming) {
|
|
499
|
+
const existing = target.get(key);
|
|
500
|
+
if (!existing) {
|
|
501
|
+
target.set(key, entry);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (sameEntry(existing, entry)) continue;
|
|
505
|
+
diagnostics.push({
|
|
506
|
+
code: "citation-duplicate-key",
|
|
507
|
+
severity: "warning",
|
|
508
|
+
message: `Duplicate bibliography key: ${key}`,
|
|
509
|
+
filePath,
|
|
510
|
+
target: key
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function sameEntry(a, b) {
|
|
515
|
+
if (a.type !== b.type) return false;
|
|
516
|
+
const aKeys = Object.keys(a.fields).toSorted();
|
|
517
|
+
const bKeys = Object.keys(b.fields).toSorted();
|
|
518
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
519
|
+
for (let index = 0; index < aKeys.length; index += 1) {
|
|
520
|
+
const key = aKeys[index] ?? "";
|
|
521
|
+
if (key !== bKeys[index]) return false;
|
|
522
|
+
if (a.fields[key] !== b.fields[key]) return false;
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
287
527
|
// src/remark.ts
|
|
528
|
+
import { visit } from "unist-util-visit";
|
|
288
529
|
var BLOCKING_ANCESTORS = /* @__PURE__ */ new Set([
|
|
289
530
|
"link",
|
|
290
531
|
"linkReference",
|
|
@@ -376,43 +617,11 @@ function bibliographyPathGroups(options, frontmatter) {
|
|
|
376
617
|
}
|
|
377
618
|
return pathList(options.bibliography).map((path) => [path]);
|
|
378
619
|
}
|
|
379
|
-
function pathList(value) {
|
|
380
|
-
if (typeof value === "string") return [value];
|
|
381
|
-
if (Array.isArray(value)) {
|
|
382
|
-
return value.filter((item) => typeof item === "string");
|
|
383
|
-
}
|
|
384
|
-
return [];
|
|
385
|
-
}
|
|
386
|
-
function pageRelativeCandidates(path, sourceSlug) {
|
|
387
|
-
const normalized = normalizeLogicalPath(path);
|
|
388
|
-
if (!sourceSlug || isAbsoluteLikePath(normalized)) return [normalized];
|
|
389
|
-
const directory = normalizeLogicalPath(sourceSlug).split("/").slice(0, -1).join("/");
|
|
390
|
-
if (!directory) return [normalized];
|
|
391
|
-
const relative = normalizeLogicalPath(`${directory}/${normalized}`);
|
|
392
|
-
return relative === normalized ? [normalized] : [relative, normalized];
|
|
393
|
-
}
|
|
394
|
-
function isAbsoluteLikePath(path) {
|
|
395
|
-
return path.startsWith("/") || /^[A-Za-z]:\//.test(path);
|
|
396
|
-
}
|
|
397
|
-
function normalizeLogicalPath(path) {
|
|
398
|
-
const parts = [];
|
|
399
|
-
for (const segment of path.replace(/\\/g, "/").split("/")) {
|
|
400
|
-
if (!segment || segment === ".") continue;
|
|
401
|
-
if (segment === "..") {
|
|
402
|
-
if (parts.length > 0 && parts.at(-1) !== "..") parts.pop();
|
|
403
|
-
else parts.push(segment);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
parts.push(segment);
|
|
407
|
-
}
|
|
408
|
-
const prefix = path.startsWith("/") ? "/" : "";
|
|
409
|
-
return `${prefix}${parts.join("/")}`;
|
|
410
|
-
}
|
|
411
620
|
async function loadBibliographies(pathGroups, options) {
|
|
412
621
|
const entries = /* @__PURE__ */ new Map();
|
|
413
622
|
const diagnostics = [];
|
|
414
623
|
for (const candidates of pathGroups) {
|
|
415
|
-
const resolved = await
|
|
624
|
+
const resolved = await loadFirstAvailable2(candidates, options);
|
|
416
625
|
if (!resolved) {
|
|
417
626
|
diagnostics.push({
|
|
418
627
|
code: "citation-missing-bibliography",
|
|
@@ -440,7 +649,7 @@ async function loadBibliographies(pathGroups, options) {
|
|
|
440
649
|
pushUniqueDiagnostics(options.diagnostics, diagnostics, options.sourceSlug);
|
|
441
650
|
return { entries, diagnostics };
|
|
442
651
|
}
|
|
443
|
-
async function
|
|
652
|
+
async function loadFirstAvailable2(candidates, options) {
|
|
444
653
|
for (const path of candidates) {
|
|
445
654
|
const resolved = await cachedBibliography(path, options);
|
|
446
655
|
if (resolved.found) return { path, parsed: resolved.parsed };
|
|
@@ -574,7 +783,7 @@ function renderCitation(references, bracketed, context) {
|
|
|
574
783
|
});
|
|
575
784
|
return {
|
|
576
785
|
type: "link",
|
|
577
|
-
url: `#${
|
|
786
|
+
url: `#${referenceId2(references[0]?.key ?? "unknown", context)}`,
|
|
578
787
|
title: null,
|
|
579
788
|
children: [{ type: "text", value: label }]
|
|
580
789
|
};
|
|
@@ -597,7 +806,7 @@ function numberForReference(reference, context) {
|
|
|
597
806
|
const number = context.orderedKeys.length + 1;
|
|
598
807
|
context.numbers.set(reference.key, number);
|
|
599
808
|
context.orderedKeys.push(reference.key);
|
|
600
|
-
|
|
809
|
+
referenceId2(reference.key, context);
|
|
601
810
|
if (!context.entries.has(reference.key)) {
|
|
602
811
|
pushUniqueDiagnostics(
|
|
603
812
|
context.options.diagnostics,
|
|
@@ -615,7 +824,7 @@ function numberForReference(reference, context) {
|
|
|
615
824
|
}
|
|
616
825
|
return number;
|
|
617
826
|
}
|
|
618
|
-
function
|
|
827
|
+
function referenceId2(key, context) {
|
|
619
828
|
const existing = context.referenceIds.get(key);
|
|
620
829
|
if (existing) return existing;
|
|
621
830
|
const sanitized = key.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -647,7 +856,7 @@ function createReferencesList(context) {
|
|
|
647
856
|
(key) => ({
|
|
648
857
|
type: "listItem",
|
|
649
858
|
spread: false,
|
|
650
|
-
data: { hProperties: { id:
|
|
859
|
+
data: { hProperties: { id: referenceId2(key, context) } },
|
|
651
860
|
children: [
|
|
652
861
|
{
|
|
653
862
|
type: "paragraph",
|
|
@@ -671,18 +880,6 @@ function renderBibliographyEntry(entry, key) {
|
|
|
671
880
|
const container = entry.fields.journal ?? entry.fields.booktitle ?? entry.fields.publisher;
|
|
672
881
|
return [author, year ? `(${year})` : void 0, title, container].filter(Boolean).join(". ");
|
|
673
882
|
}
|
|
674
|
-
function pushUniqueDiagnostics(target, diagnostics, slug) {
|
|
675
|
-
for (const diagnostic of diagnostics) {
|
|
676
|
-
const next = diagnostic.slug ? diagnostic : { ...diagnostic, slug };
|
|
677
|
-
const signature = diagnosticSignature(next);
|
|
678
|
-
if (target.some((item) => diagnosticSignature(item) === signature))
|
|
679
|
-
continue;
|
|
680
|
-
target.push(next);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function diagnosticSignature(diagnostic) {
|
|
684
|
-
return `${diagnostic.code}:${diagnostic.slug ?? ""}:${diagnostic.filePath ?? ""}:${diagnostic.target ?? ""}:${diagnostic.message}`;
|
|
685
|
-
}
|
|
686
883
|
|
|
687
884
|
// src/state.ts
|
|
688
885
|
function createCitationState() {
|
|
@@ -690,11 +887,11 @@ function createCitationState() {
|
|
|
690
887
|
}
|
|
691
888
|
|
|
692
889
|
// index.ts
|
|
693
|
-
var
|
|
890
|
+
var PLUGIN_NAME2 = "citations";
|
|
694
891
|
function citations(options = {}) {
|
|
695
892
|
const state = createCitationState();
|
|
696
893
|
return definePlugin({
|
|
697
|
-
name:
|
|
894
|
+
name: PLUGIN_NAME2,
|
|
698
895
|
processedContentCache: {
|
|
699
896
|
version: "citations-v1",
|
|
700
897
|
dependencyMode: "tracked"
|
|
@@ -713,7 +910,7 @@ function citations(options = {}) {
|
|
|
713
910
|
diagnostics: state.diagnostics,
|
|
714
911
|
loadBibliography: async (path) => {
|
|
715
912
|
if (!context.contentSource) return null;
|
|
716
|
-
const content = await
|
|
913
|
+
const content = await readContentSourceEntry2(
|
|
717
914
|
context.contentSource,
|
|
718
915
|
path
|
|
719
916
|
);
|
|
@@ -723,6 +920,32 @@ function citations(options = {}) {
|
|
|
723
920
|
cache: state.bibliographies
|
|
724
921
|
});
|
|
725
922
|
},
|
|
923
|
+
pageTypes: [
|
|
924
|
+
{
|
|
925
|
+
id: "citations-bibliography",
|
|
926
|
+
paths: (context) => options.pagePath === void 0 ? [] : listBibliographyPagePaths(
|
|
927
|
+
{
|
|
928
|
+
manifest: context.manifest,
|
|
929
|
+
contentSource: context.contentSource,
|
|
930
|
+
diagnostics: context.diagnostics
|
|
931
|
+
},
|
|
932
|
+
options
|
|
933
|
+
),
|
|
934
|
+
outputDependencies: [{ type: "global" }],
|
|
935
|
+
resolve: (context) => options.pagePath === void 0 ? null : resolveBibliographyPage(
|
|
936
|
+
{
|
|
937
|
+
manifest: context.manifest,
|
|
938
|
+
contentSource: context.contentSource,
|
|
939
|
+
diagnostics: context.diagnostics,
|
|
940
|
+
pathname: context.pathname
|
|
941
|
+
},
|
|
942
|
+
options
|
|
943
|
+
)
|
|
944
|
+
}
|
|
945
|
+
],
|
|
946
|
+
assets: [
|
|
947
|
+
createStyleAsset(PLUGIN_NAME2, "@riebeckite/plugin-citations/style.css")
|
|
948
|
+
],
|
|
726
949
|
addDiagnostics: () => state.diagnostics
|
|
727
950
|
});
|
|
728
951
|
}
|
|
@@ -743,6 +966,9 @@ function validateCitationsOptions(options) {
|
|
|
743
966
|
if (options.referencesHeading !== void 0 && typeof options.referencesHeading !== "string") {
|
|
744
967
|
issues.push({ path: "referencesHeading", message: "Expected a string." });
|
|
745
968
|
}
|
|
969
|
+
if (options.pagePath !== void 0 && typeof options.pagePath !== "string") {
|
|
970
|
+
issues.push({ path: "pagePath", message: "Expected a string." });
|
|
971
|
+
}
|
|
746
972
|
return issues;
|
|
747
973
|
}
|
|
748
974
|
export {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ContentManifest, type ContentSource, type Diagnostic, type PluginPage } from "@riebeckite/core";
|
|
2
|
+
import type { CitationsOptions } from "./types.js";
|
|
3
|
+
export type BibliographyPageSource = {
|
|
4
|
+
readonly manifest: ContentManifest;
|
|
5
|
+
readonly contentSource?: ContentSource;
|
|
6
|
+
readonly diagnostics: Diagnostic[];
|
|
7
|
+
};
|
|
8
|
+
export declare function listBibliographyPagePaths(source: BibliographyPageSource, options: CitationsOptions): Promise<string[]>;
|
|
9
|
+
export declare function resolveBibliographyPage(context: BibliographyPageSource & {
|
|
10
|
+
readonly pathname: string;
|
|
11
|
+
}, options: CitationsOptions): Promise<PluginPage | null>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Diagnostic } from "@riebeckite/core";
|
|
2
|
+
export declare function pathList(value: unknown): readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* Frontmatter paths are document-relative first, then content-root relative.
|
|
5
|
+
* Every candidate still goes through `ContentSource.scan()`, so no candidate can
|
|
6
|
+
* escape the configured content root.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pageRelativeCandidates(path: string, sourceSlug: string | undefined): readonly string[];
|
|
9
|
+
export declare function isAbsoluteLikePath(path: string): boolean;
|
|
10
|
+
export declare function normalizeLogicalPath(path: string): string;
|
|
11
|
+
export declare function pushUniqueDiagnostics(target: Diagnostic[], diagnostics: readonly Diagnostic[], slug: string | undefined): void;
|
|
12
|
+
export declare function diagnosticSignature(diagnostic: Diagnostic): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BibliographyEntry } from "./types.js";
|
|
2
|
+
export type RenderBibliographyInput = {
|
|
3
|
+
readonly title: string;
|
|
4
|
+
readonly entries: readonly (readonly [string, BibliographyEntry])[];
|
|
5
|
+
};
|
|
6
|
+
export declare function renderBibliographyPage(input: RenderBibliographyInput): string;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type CitationsOptions = {
|
|
|
4
4
|
bibliography?: string | readonly string[];
|
|
5
5
|
/** Heading inserted above generated references. Defaults to "References". */
|
|
6
6
|
referencesHeading?: string;
|
|
7
|
+
pagePath?: string;
|
|
7
8
|
};
|
|
8
9
|
export type BibliographyEntry = {
|
|
9
10
|
readonly key: string;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-citations",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@types/mdast": "^4.0.4",
|
|
9
9
|
"unist-util-visit": "^5.1.0",
|
|
10
|
-
"@riebeckite/core": "0.0.
|
|
10
|
+
"@riebeckite/core": "0.0.23"
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"LICENSE",
|
|
17
17
|
"README.md",
|
|
18
18
|
"README_ja.md",
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"style.css"
|
|
20
21
|
],
|
|
21
22
|
"exports": {
|
|
22
23
|
".": {
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
"import": "./dist/index.js",
|
|
25
26
|
"source": "./index.ts",
|
|
26
27
|
"default": "./dist/index.js"
|
|
27
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"./style.css": "./style.css"
|
|
28
30
|
},
|
|
29
31
|
"repository": {
|
|
30
32
|
"type": "git",
|
|
@@ -46,7 +48,9 @@
|
|
|
46
48
|
"engines": {
|
|
47
49
|
"node": "^20.19.0 || >=22.12.0"
|
|
48
50
|
},
|
|
49
|
-
"sideEffects":
|
|
51
|
+
"sideEffects": [
|
|
52
|
+
"./style.css"
|
|
53
|
+
],
|
|
50
54
|
"scripts": {
|
|
51
55
|
"build": "node ../../../scripts/build_package.mjs",
|
|
52
56
|
"test": "node --import tsx --test \"test/*.test.ts\""
|
package/style.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.rb-citations-page {
|
|
2
|
+
width: min(100%, 48rem);
|
|
3
|
+
margin-inline: auto;
|
|
4
|
+
padding-inline: var(--rb-space-2, 1rem);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.rb-citations-page__title {
|
|
8
|
+
margin-block-end: var(--rb-space-2, 1rem);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.rb-citations-page__list {
|
|
12
|
+
display: grid;
|
|
13
|
+
gap: var(--rb-space-1, 0.5rem);
|
|
14
|
+
padding-inline-start: var(--rb-space-2, 1rem);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.rb-citations-page__item {
|
|
18
|
+
line-height: 1.6;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.rb-citations-page__year,
|
|
22
|
+
.rb-citations-page__container {
|
|
23
|
+
opacity: 0.75;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.rb-citations-page__link {
|
|
27
|
+
overflow-wrap: anywhere;
|
|
28
|
+
}
|