@takagaki/cortex-decisions-viewer 0.12.145 → 0.12.147
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/dist/build.js +37 -1
- package/dist/render.js +76 -12
- package/dist/story.js +95 -3
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -60,6 +60,7 @@ exports.decidedByOf = decidedByOf;
|
|
|
60
60
|
exports.isRelatedUrl = isRelatedUrl;
|
|
61
61
|
exports.relatedMaterialsRead = relatedMaterialsRead;
|
|
62
62
|
exports.relatedMaterialsView = relatedMaterialsView;
|
|
63
|
+
exports.attachRuleReferences = attachRuleReferences;
|
|
63
64
|
exports.attachRelatedMaterials = attachRelatedMaterials;
|
|
64
65
|
exports.createRefResolver = createRefResolver;
|
|
65
66
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -126,6 +127,27 @@ const DEFAULT_EXCLUDE = ["README.md", "template.md"];
|
|
|
126
127
|
* `supersedes` の写しで、描くと同じ置き換えが逆向きにもう 1 本出る。frontmatter のパネルには今までどおり出る
|
|
127
128
|
*/
|
|
128
129
|
exports.MIRROR_RELS = new Set(["superseded_by"]);
|
|
130
|
+
/**
|
|
131
|
+
* ストーリーの名前のリンクの材料(`story.screen-name-links`)。関係者・用語・ルールの記録ごとに、表示名と別名・同義語を集める。
|
|
132
|
+
* 画面に載る記録(`sections` に残った物)だけなので、取り下げ・削除された記録の id は表に無く、リンクにならない。
|
|
133
|
+
* 関係者の題は姓名の間に空白があることがあるので、空白を除いた形も名前に足す
|
|
134
|
+
*/
|
|
135
|
+
function storyNameSources(sections) {
|
|
136
|
+
const out = new Map();
|
|
137
|
+
const list = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string") : []);
|
|
138
|
+
for (const sec of sections) {
|
|
139
|
+
for (const r of sec.records) {
|
|
140
|
+
const kind = r.id.split(":")[0];
|
|
141
|
+
if (kind !== "member" && kind !== "term" && kind !== "rule")
|
|
142
|
+
continue;
|
|
143
|
+
const names = [r.title, ...list(r.fm.aliases), ...list(r.fm.synonyms)];
|
|
144
|
+
if (kind === "member")
|
|
145
|
+
names.push(r.title.replace(/\s+/g, ""));
|
|
146
|
+
out.set(r.id, { id: r.id, names });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
129
151
|
/** frontmatterのrelationsを安全に正規化する */
|
|
130
152
|
function toRelations(value) {
|
|
131
153
|
if (!Array.isArray(value))
|
|
@@ -2446,6 +2468,8 @@ async function buildSite(opts, views) {
|
|
|
2446
2468
|
records = records.filter((r) => !r.relations.some((x) => x.rel === "retracted_by"));
|
|
2447
2469
|
// 関連の資料(cortex-engine #1094 F8)。ルール・用語だけ・欄の無い記録には何も付かない
|
|
2448
2470
|
attachRelatedMaterials(e.name, records, refs);
|
|
2471
|
+
// ルールの参照(rule.card)。決定と同じ出典リンクにする
|
|
2472
|
+
attachRuleReferences(e.name, records, refs);
|
|
2449
2473
|
// 用語集の並び(活性度・強さ)。**帳簿を持たない案件では並びもそのまま**
|
|
2450
2474
|
const rederived = orderRecordsByLedgers(records, recordsDir, rederive, opts.generatedAt);
|
|
2451
2475
|
// 関係者だけ、関与の多い順(engine の目次と同じ `order`・便 14b)。用語集の並び
|
|
@@ -2514,7 +2538,9 @@ async function buildSite(opts, views) {
|
|
|
2514
2538
|
// ストーリーの日々の動き(#901 V2-5)。帳簿で on の日のファイルが無ければ null(キーごと出さない)
|
|
2515
2539
|
const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null,
|
|
2516
2540
|
// 動きのない日を埋める終点=ビルドの時刻の日本時間の日付(時計を注入できる)
|
|
2517
|
-
storyToday, ledgerDir
|
|
2541
|
+
storyToday, ledgerDir,
|
|
2542
|
+
// 名前のリンク(story.screen-name-links): 日の relations の関係者・用語・ルールを、画面にある記録の表示名・別名・同義語で本文に当てる
|
|
2543
|
+
(targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)));
|
|
2518
2544
|
// 週・月のあらすじ(#901)。週・月のファイルが無ければ null(キーごと出さない=今までと同じ画面)
|
|
2519
2545
|
const storyDigest = await (0, story_1.readStoryDigest)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null, storyToday, ledgerDir);
|
|
2520
2546
|
// 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
|
|
@@ -2781,6 +2807,16 @@ function relatedMaterialsView(fm, refs) {
|
|
|
2781
2807
|
}
|
|
2782
2808
|
/** 関連の資料を持てる汎用の区画(関係者・ストーリーの日・Home の欄は拾わない) */
|
|
2783
2809
|
exports.RELATED_MATERIALS_SECTIONS = new Set(["Rules", "Glossary"]);
|
|
2810
|
+
/** ルールの `references` を決定と同じ出典リンクにして載せる(rule.card)。ルールだけ・欄の無い記録にはキーごと付けない */
|
|
2811
|
+
function attachRuleReferences(section, records, refs) {
|
|
2812
|
+
if (section !== "Rules")
|
|
2813
|
+
return;
|
|
2814
|
+
for (const r of records) {
|
|
2815
|
+
const list = toStringArray(r.fm.references).map((x) => refs.resolve(x));
|
|
2816
|
+
if (list.length)
|
|
2817
|
+
r.references = list;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2784
2820
|
/** 区画の記録に関連の資料を載せる(ルール・用語だけ・欄の無い記録にはキーごと付けない) */
|
|
2785
2821
|
function attachRelatedMaterials(section, records, refs) {
|
|
2786
2822
|
if (!exports.RELATED_MATERIALS_SECTIONS.has(section))
|
package/dist/render.js
CHANGED
|
@@ -89,6 +89,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
89
89
|
const google_chat_guide_images_1 = require("./google-chat-guide-images");
|
|
90
90
|
const meet_guide_images_1 = require("./meet-guide-images");
|
|
91
91
|
const figma_guide_images_1 = require("./figma-guide-images");
|
|
92
|
+
const story_1 = require("./story");
|
|
92
93
|
// 「人が確認したか/機械が触ってよいか」の口(#699 F2b)。正本は engine の confirmed.mjs・判定は流し込むだけ
|
|
93
94
|
const confirmed_1 = require("./confirmed");
|
|
94
95
|
// フロントマターの「つながっている ID」の欄名(#686)。**綴りはここ 1 本**——読む側(画面)も
|
|
@@ -6537,6 +6538,8 @@ ${calendarAddUrl.toString()}
|
|
|
6537
6538
|
try { return elx ? JSON.parse(elx.textContent) : null; } catch (e) { return null; }
|
|
6538
6539
|
})();
|
|
6539
6540
|
|
|
6541
|
+
// ストーリーの名前のリンクの切り分け(story.ts の純関数をそのまま埋め込む)
|
|
6542
|
+
${story_1.splitByNameLinks.toString()}
|
|
6540
6543
|
// ---------- インデックス構築 ----------
|
|
6541
6544
|
var byId = {};
|
|
6542
6545
|
SITE.decisions.forEach(function (d) { byId[d.id] = d; d._kind = "decision"; });
|
|
@@ -6565,6 +6568,38 @@ ${calendarAddUrl.toString()}
|
|
|
6565
6568
|
return '<a class="wikilink" data-go="' + attr + '">' + disp + "</a>";
|
|
6566
6569
|
});
|
|
6567
6570
|
}
|
|
6571
|
+
// ストーリーの本文の名前を記録へのリンクにする(story.screen-name-links)。
|
|
6572
|
+
// 🔴 本文の DOM を作った後に**テキストノードだけ**を走査する(HTML の文字列は触らない=タグ・属性は壊れない)。
|
|
6573
|
+
// a・code・pre・既存のウィキリンクの中は当てない。飛び先(byId)が無い id は当てない(素の文字のまま=空リンク・行き止まりを出さない)。
|
|
6574
|
+
// 名前は textContent で入れる(表示名を HTML として混ぜない)
|
|
6575
|
+
function linkifyStoryNames(root, links) {
|
|
6576
|
+
if (!root || !links || !links.length) return;
|
|
6577
|
+
var usable = links.filter(function (l) { return l && l.name && l.id && byId[l.id] && !byId[l.id].deleted; });
|
|
6578
|
+
if (!usable.length) return;
|
|
6579
|
+
var skip = { A: 1, CODE: 1, PRE: 1, SCRIPT: 1, STYLE: 1 };
|
|
6580
|
+
var nodes = [];
|
|
6581
|
+
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
6582
|
+
acceptNode: function (n) {
|
|
6583
|
+
for (var p = n.parentNode; p && p !== root; p = p.parentNode) { if (skip[p.tagName]) return NodeFilter.FILTER_REJECT; }
|
|
6584
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
6585
|
+
}
|
|
6586
|
+
});
|
|
6587
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
6588
|
+
nodes.forEach(function (n) {
|
|
6589
|
+
var parts = splitByNameLinks(n.nodeValue, usable);
|
|
6590
|
+
if (parts.length === 1 && !parts[0].id) return;
|
|
6591
|
+
var frag = document.createDocumentFragment();
|
|
6592
|
+
parts.forEach(function (part) {
|
|
6593
|
+
if (!part.id) { frag.appendChild(document.createTextNode(part.text)); return; }
|
|
6594
|
+
var a = document.createElement("a");
|
|
6595
|
+
a.className = "wikilink";
|
|
6596
|
+
a.setAttribute("data-go", part.id);
|
|
6597
|
+
a.textContent = part.text;
|
|
6598
|
+
frag.appendChild(a);
|
|
6599
|
+
});
|
|
6600
|
+
n.parentNode.replaceChild(frag, n);
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6568
6603
|
// 本文は bodies/ 以下へ外出しされ詳細表示時に fetch されるため、linkify は loadBody 側で適用する
|
|
6569
6604
|
// (ここで inline bodyHtml に適用すると検索テキストにHTMLが混ざるため適用しない)。
|
|
6570
6605
|
|
|
@@ -6984,6 +7019,24 @@ ${calendarAddUrl.toString()}
|
|
|
6984
7019
|
box.appendChild(ul);
|
|
6985
7020
|
return box;
|
|
6986
7021
|
}
|
|
7022
|
+
/**
|
|
7023
|
+
* 詳細の節「参照」(根拠の出典リンク)。決定とルールが同じ出し方(rule.card・出典リンク)。
|
|
7024
|
+
* 参照の無い記録では null(要素を作らない)。リンクは http(s) だけ(ビルドが解決済みでも画面で二重に見る)
|
|
7025
|
+
*/
|
|
7026
|
+
function referencesSection(r) {
|
|
7027
|
+
if (!r || !r.references || !r.references.length) return null;
|
|
7028
|
+
var box = el("div", { class: "refs-section" });
|
|
7029
|
+
box.appendChild(el("div", { class: "section-label", text: "参照" }));
|
|
7030
|
+
var ul = el("ul", { class: "refs" });
|
|
7031
|
+
r.references.forEach(function (ref) {
|
|
7032
|
+
var li = el("li");
|
|
7033
|
+
if (ref.url && /^https?:\\/\\//i.test(ref.url)) li.appendChild(el("a", { href: ref.url, target: "_blank", rel: "noopener", text: ref.label }));
|
|
7034
|
+
else li.textContent = ref.label;
|
|
7035
|
+
ul.appendChild(li);
|
|
7036
|
+
});
|
|
7037
|
+
box.appendChild(ul);
|
|
7038
|
+
return box;
|
|
7039
|
+
}
|
|
6987
7040
|
/** 候補の帯(症状)。行の札は「候補」とだけ言い、理由はこの帯 1 行に寄せる(繰り返さない) */
|
|
6988
7041
|
var SUPERSEDE_CANDIDATE_NOTE = "前の決定は人が直しているため、置き換えは候補です";
|
|
6989
7042
|
/** 決定 1 件の行。SITE にあれば題名で詳細へのリンク、無ければ ID だけ */
|
|
@@ -7013,6 +7066,21 @@ ${calendarAddUrl.toString()}
|
|
|
7013
7066
|
for (var i = 0; i < by.length; i++) if (by[i].id === d.id) return !by[i].confirmed;
|
|
7014
7067
|
return false;
|
|
7015
7068
|
}
|
|
7069
|
+
/**
|
|
7070
|
+
* 一覧のカードの「→ 変更先」(decision.show-superseded)。置き換えられた決定の行に、一番新しい置き換え先の題名を 1 行で添える。
|
|
7071
|
+
* 置き換え先が複数なら「ほか N 件」。置き換えられていない決定では null(行を作らない)。
|
|
7072
|
+
* 札(置き換え済み・最新の生成)は出さない(common.screen-no-state-label)。題名は textContent で入れる
|
|
7073
|
+
*/
|
|
7074
|
+
function supersededToLine(d) {
|
|
7075
|
+
var by = (d && d.supersededBy && d.supersededBy.by) || [];
|
|
7076
|
+
if (!by.length) return null;
|
|
7077
|
+
// 画面で削除済みの置き換え先は題名を出さない(一覧から外れた決定を指さない)。見つからなければ ID の文字
|
|
7078
|
+
var shown = by.filter(function (b) { var n = byId[b.id]; return !(n && n.deleted); });
|
|
7079
|
+
var first = shown.length ? shown[0] : by[0];
|
|
7080
|
+
var next = byId[first.id];
|
|
7081
|
+
var name = next && next._kind === "decision" && !next.deleted ? humanTitle(next.title) : first.id;
|
|
7082
|
+
return el("p", { class: "card-superseded-to", text: "→ 変更先: " + name + (by.length > 1 ? " ほか " + (by.length - 1) + " 件" : "") });
|
|
7083
|
+
}
|
|
7016
7084
|
/** 詳細の節「置き換え先の決定」「置き換えた決定」。どちらも無ければ null(要素を作らない) */
|
|
7017
7085
|
function supersedeSection(d) {
|
|
7018
7086
|
var by = (d && d.supersededBy && d.supersededBy.by) || [];
|
|
@@ -8659,7 +8727,7 @@ ${calendarAddUrl.toString()}
|
|
|
8659
8727
|
}
|
|
8660
8728
|
list.appendChild(el("section", { class: "story-card" }, [
|
|
8661
8729
|
el("h3", { class: "story-card-heading", text: d.heading || d.date }),
|
|
8662
|
-
d.bodyHtml ? el("div", { class: "body", html: d.bodyHtml }) : null,
|
|
8730
|
+
d.bodyHtml ? (function () { var b = el("div", { class: "body", html: d.bodyHtml }); linkifyStoryNames(b, d.links); return b; })() : null,
|
|
8663
8731
|
// その日のファイルを GitHub で開く(表示の文字にファイル名を出さない)。URL が無い案件は出さない
|
|
8664
8732
|
storyEditRow(STORY_DAYS_BASE + d.date.slice(0, 7) + "/" + d.date + ".md", d.editUrl),
|
|
8665
8733
|
]));
|
|
@@ -19721,6 +19789,7 @@ ${calendarAddUrl.toString()}
|
|
|
19721
19789
|
// 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
|
|
19722
19790
|
var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, head.concat([
|
|
19723
19791
|
d.summary ? el("p", { class: "card-summary", text: d.summary }) : null,
|
|
19792
|
+
supersededToLine(d),
|
|
19724
19793
|
]));
|
|
19725
19794
|
card.addEventListener("click", function () { go(d.id); });
|
|
19726
19795
|
wrap.appendChild(card);
|
|
@@ -20591,6 +20660,10 @@ ${calendarAddUrl.toString()}
|
|
|
20591
20660
|
app.appendChild(genBody);
|
|
20592
20661
|
loadBody(r, genBody);
|
|
20593
20662
|
|
|
20663
|
+
// 参照(rule.card)。ルールだけ。決定の詳細と同じ「参照」の節(ビルドが解決した出典リンク)
|
|
20664
|
+
var refs2 = referencesSection(r);
|
|
20665
|
+
if (refs2) app.appendChild(refs2);
|
|
20666
|
+
|
|
20594
20667
|
// 関連の資料(#1094 F8)。本文の後。ルール・用語にしか載らない
|
|
20595
20668
|
var relm2 = relatedMaterialsSection(r);
|
|
20596
20669
|
if (relm2) app.appendChild(relm2);
|
|
@@ -21544,17 +21617,8 @@ ${calendarAddUrl.toString()}
|
|
|
21544
21617
|
app.appendChild(decBody);
|
|
21545
21618
|
loadBody(d, decBody);
|
|
21546
21619
|
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
var ul = el("ul", { class: "refs" });
|
|
21550
|
-
d.references.forEach(function (r) {
|
|
21551
|
-
var li = el("li");
|
|
21552
|
-
if (r.url) li.appendChild(el("a", { href: r.url, target: "_blank", rel: "noopener", text: r.label }));
|
|
21553
|
-
else li.textContent = r.label;
|
|
21554
|
-
ul.appendChild(li);
|
|
21555
|
-
});
|
|
21556
|
-
app.appendChild(ul);
|
|
21557
|
-
}
|
|
21620
|
+
var refsBox = referencesSection(d);
|
|
21621
|
+
if (refsBox) app.appendChild(refsBox);
|
|
21558
21622
|
|
|
21559
21623
|
// 関連の資料(#1094 F8)。「参照」(根拠)の後・置き換えの前。根拠と混ぜない
|
|
21560
21624
|
var relm = relatedMaterialsSection(d);
|
package/dist/story.js
CHANGED
|
@@ -3,13 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.STORY_DIGEST_EMPTY_LINE = exports.STORY_MAX_MONTHS = exports.STORY_MAX_WEEKS = exports.STORY_MONTHS_DIR = exports.STORY_WEEKS_DIR = exports.STORY_MAX_FILE_BYTES = exports.STORY_COLLAPSE_QUIET_DAYS = exports.STORY_EMPTY_LINE = exports.STORY_MAX_DAYS = exports.STORY_DAYS_DIR = exports.STORY_LEDGER_FILE = void 0;
|
|
6
|
+
exports.STORY_DIGEST_EMPTY_LINE = exports.STORY_MAX_MONTHS = exports.STORY_MAX_WEEKS = exports.STORY_MONTHS_DIR = exports.STORY_WEEKS_DIR = exports.STORY_LINK_MIN_NAME = exports.STORY_LINK_KINDS = exports.STORY_MAX_FILE_BYTES = exports.STORY_COLLAPSE_QUIET_DAYS = exports.STORY_EMPTY_LINE = exports.STORY_MAX_DAYS = exports.STORY_DAYS_DIR = exports.STORY_LEDGER_FILE = void 0;
|
|
7
7
|
exports.parseStoryLedger = parseStoryLedger;
|
|
8
8
|
exports.partialDaysOf = partialDaysOf;
|
|
9
9
|
exports.onDaysOf = onDaysOf;
|
|
10
10
|
exports.successDaysOf = successDaysOf;
|
|
11
11
|
exports.nightResultOf = nightResultOf;
|
|
12
12
|
exports.splitFrontmatter = splitFrontmatter;
|
|
13
|
+
exports.dayRelationTargets = dayRelationTargets;
|
|
14
|
+
exports.storyNameLinks = storyNameLinks;
|
|
15
|
+
exports.splitByNameLinks = splitByNameLinks;
|
|
13
16
|
exports.storyDayMarkdown = storyDayMarkdown;
|
|
14
17
|
exports.isEngineQuietDay = isEngineQuietDay;
|
|
15
18
|
exports.todayJst = todayJst;
|
|
@@ -162,6 +165,88 @@ function splitFrontmatter(content) {
|
|
|
162
165
|
return { frontmatter: null, body: text };
|
|
163
166
|
return { frontmatter: m[1] ?? "", body: text.slice(m[0].length) };
|
|
164
167
|
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// 名前のリンク(`story.screen-name-links`)。日のページの `relations` で当てる(本文からは名前を拾わない)
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
/** 名前のリンクにする記録の種類(id の頭)。同じ名前が複数の種類にあるときはこの順で勝つ(関係者 → 用語 → ルール・設計 A4) */
|
|
172
|
+
exports.STORY_LINK_KINDS = ["member", "term", "rule"];
|
|
173
|
+
/** 名前の最小の長さ(1 文字の名前は本文のあちこちに当たるので当てない) */
|
|
174
|
+
exports.STORY_LINK_MIN_NAME = 2;
|
|
175
|
+
/**
|
|
176
|
+
* 日のページの frontmatter から `relations` の `target` を取る(純関数・YAML としては読まない)。
|
|
177
|
+
* `- { rel: relates_to, target: "term:設計" }` も、`- rel:` と `target:` を行で分けた形も読む。壊れた行は飛ばす
|
|
178
|
+
*/
|
|
179
|
+
function dayRelationTargets(frontmatter) {
|
|
180
|
+
if (!frontmatter)
|
|
181
|
+
return [];
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const line of frontmatter.split("\n")) {
|
|
184
|
+
const m = line.match(/\btarget\s*:\s*(?:"([^"]*)"|'([^']*)'|([^\s,}"']+))/);
|
|
185
|
+
const t = (m ? (m[1] ?? m[2] ?? m[3] ?? "") : "").trim();
|
|
186
|
+
if (t && !out.includes(t))
|
|
187
|
+
out.push(t);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* その日の `relations` の target から、名前のリンクの表を作る(純関数)。
|
|
193
|
+
*
|
|
194
|
+
* - 材料(`sources`)に **無い id は捨てる**(廃止・削除された記録、実在しない id、記録ではない id=決定など)。飛び先の無い線は出さない
|
|
195
|
+
* - 同じ名前が複数の記録にあれば、関係者 → 用語 → ルールの順で勝つ。同じ種類なら先に出た target
|
|
196
|
+
* - 2 文字未満の名前は当てない。返りは名前の長い順(同じ長さは文字の順)。1 件も無ければ空
|
|
197
|
+
*/
|
|
198
|
+
function storyNameLinks(targets, sources) {
|
|
199
|
+
const kindOf = (id) => exports.STORY_LINK_KINDS.findIndex((k) => id.startsWith(`${k}:`));
|
|
200
|
+
const ids = [...new Set(targets)].filter((t) => kindOf(t) >= 0 && sources.has(t));
|
|
201
|
+
ids.sort((a, b) => kindOf(a) - kindOf(b));
|
|
202
|
+
const byName = new Map();
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
for (const raw of sources.get(id).names) {
|
|
205
|
+
const name = String(raw ?? "").trim();
|
|
206
|
+
if (name.length >= exports.STORY_LINK_MIN_NAME && !byName.has(name))
|
|
207
|
+
byName.set(name, id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return [...byName.entries()]
|
|
211
|
+
.map(([name, id]) => ({ name, id }))
|
|
212
|
+
.sort((a, b) => b.name.length - a.name.length || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 画面側の純関数: 1 つの文字列を、名前に当たる所とそれ以外に分ける。**画面の JS にそのまま埋め込む**ので、このファイルの外の物を使わない。
|
|
216
|
+
* 長い名前を先に当てる(`links` は長い順で渡す)。当たらなければ 1 件(元の文字)を返す
|
|
217
|
+
*/
|
|
218
|
+
function splitByNameLinks(text, links) {
|
|
219
|
+
if (!text || !links || !links.length)
|
|
220
|
+
return [{ text: text }];
|
|
221
|
+
const byName = {};
|
|
222
|
+
const names = [];
|
|
223
|
+
links.forEach((l) => {
|
|
224
|
+
if (l && l.name && l.id && !Object.prototype.hasOwnProperty.call(byName, l.name)) {
|
|
225
|
+
byName[l.name] = l.id;
|
|
226
|
+
names.push(l.name);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
names.sort((a, b) => b.length - a.length);
|
|
230
|
+
// 英数字で始まる・終わる名前は、前後が英数字のときは当てない(`AI` が `MAIN` の途中に当たらない)。日本語の名前は途中でも当てる
|
|
231
|
+
const word = (n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
232
|
+
const re = new RegExp(names.map((n) => (/^[A-Za-z0-9_]/.test(n) ? "(?<![A-Za-z0-9_])" : "") + word(n) + (/[A-Za-z0-9_]$/.test(n) ? "(?![A-Za-z0-9_])" : "")).join("|"), "g");
|
|
233
|
+
const out = [];
|
|
234
|
+
let last = 0;
|
|
235
|
+
let m;
|
|
236
|
+
while ((m = re.exec(text)) !== null) {
|
|
237
|
+
if (!m[0]) {
|
|
238
|
+
re.lastIndex++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (m.index > last)
|
|
242
|
+
out.push({ text: text.slice(last, m.index) });
|
|
243
|
+
out.push({ text: m[0], id: byName[m[0]] });
|
|
244
|
+
last = m.index + m[0].length;
|
|
245
|
+
}
|
|
246
|
+
if (last < text.length)
|
|
247
|
+
out.push({ text: text.slice(last) });
|
|
248
|
+
return out.length ? out : [{ text: text }];
|
|
249
|
+
}
|
|
165
250
|
/** 曜日つきの見出し(`# 2026-09-24(木)`)の文字。無ければ日付だけ */
|
|
166
251
|
function headingOf(lines, day) {
|
|
167
252
|
for (const l of lines) {
|
|
@@ -372,7 +457,9 @@ editUrlOf = () => null,
|
|
|
372
457
|
// 日本時間の今日(テストは時計を注入する)
|
|
373
458
|
today = todayJst(),
|
|
374
459
|
// 帳簿の置き場(既定は `<repoRoot>/.cortex`)
|
|
375
|
-
ledgerDir
|
|
460
|
+
ledgerDir,
|
|
461
|
+
// 日の `relations` の target から名前のリンクの表を作る口(build が記録の表を握って渡す。無ければリンクは付けない)
|
|
462
|
+
linksOf = () => []) {
|
|
376
463
|
const ledger = await loadStoryLedger(repoRoot, true, ledgerDir);
|
|
377
464
|
if (!ledger)
|
|
378
465
|
return null;
|
|
@@ -432,7 +519,12 @@ ledgerDir) {
|
|
|
432
519
|
continue;
|
|
433
520
|
const { heading, markdown } = storyDayMarkdown(raw, f.day);
|
|
434
521
|
const editUrl = editUrlOf(f.rel);
|
|
435
|
-
|
|
522
|
+
const links = linksOf(dayRelationTargets(splitFrontmatter(raw).frontmatter));
|
|
523
|
+
out.push({
|
|
524
|
+
date: f.day, heading: heading, bodyHtml: await renderMarkdown(markdown),
|
|
525
|
+
...(editUrl ? { editUrl: editUrl } : {}),
|
|
526
|
+
...(links.length ? { links: links } : {}),
|
|
527
|
+
});
|
|
436
528
|
}
|
|
437
529
|
// 一番古い「動きのある日」から今日まで、日付の抜けを動きのない日の行で埋める。動きのある日が 1 つも無ければ出さない
|
|
438
530
|
// 抜けの日は、帳簿の「成功した日」(行があり・on で・途中まででない日)なら動きなし、そうでなければ更新の失敗として出す
|
package/package.json
CHANGED