2026-04-04 15:51:53 +00:00
|
|
|
import { describe, it, mock } from "node:test";
|
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import expect from "expect";
|
|
|
|
|
import { TFile } from "obsidian";
|
|
|
|
|
import {
|
|
|
|
|
addLinkToSection,
|
|
|
|
|
removeLinkFromSection,
|
|
|
|
|
getLinksInSection,
|
|
|
|
|
getSectionContent,
|
|
|
|
|
getAllSectionData,
|
|
|
|
|
setSectionContent,
|
|
|
|
|
addBacklinkToFile,
|
|
|
|
|
} from "../src/sections";
|
|
|
|
|
|
|
|
|
|
/** Build a minimal mock App backed by an in-memory string. */
|
|
|
|
|
function makeApp(filePath: string, initialContent: string) {
|
|
|
|
|
let stored = initialContent;
|
|
|
|
|
|
|
|
|
|
const file = Object.create(TFile.prototype) as TFile;
|
|
|
|
|
file.path = filePath;
|
|
|
|
|
|
|
|
|
|
const app = {
|
|
|
|
|
vault: {
|
|
|
|
|
getAbstractFileByPath: (p: string) => (p === filePath ? file : null),
|
|
|
|
|
read: mock.fn(async () => stored),
|
|
|
|
|
process: mock.fn(async (_f: unknown, fn: (s: string) => string) => { stored = fn(stored); return stored; }),
|
|
|
|
|
},
|
|
|
|
|
metadataCache: {
|
|
|
|
|
getFileCache: mock.fn(() => null),
|
|
|
|
|
getFirstLinkpathDest: mock.fn(() => null),
|
|
|
|
|
fileToLinktext: mock.fn((f: TFile) => f.path),
|
|
|
|
|
},
|
|
|
|
|
} as unknown as import("obsidian").App;
|
|
|
|
|
|
|
|
|
|
return { app, getContent: () => stored };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── addLinkToSection ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("addLinkToSection", () => {
|
|
|
|
|
it("appends a link under an existing section", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Towns\n\n[[Riverdale]]\n");
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Millhaven]]");
|
|
|
|
|
expect(getContent()).toContain("[[Millhaven]]");
|
|
|
|
|
expect(getContent()).toContain("[[Riverdale]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("creates the section when it does not exist", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "Some content.");
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Newtown]]");
|
|
|
|
|
expect(getContent()).toContain("### Towns");
|
|
|
|
|
expect(getContent()).toContain("[[Newtown]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not add a duplicate link", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Towns\n\n[[Riverdale]]\n");
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Riverdale]]");
|
|
|
|
|
const count = (getContent().match(/\[\[Riverdale\]\]/g) ?? []).length;
|
|
|
|
|
expect(count).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not modify other sections", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Dungeons\n\n[[Cave]]\n\n### Towns\n\n");
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Village]]");
|
|
|
|
|
expect(getContent()).toContain("[[Cave]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is a no-op when file does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "");
|
|
|
|
|
// Should not throw
|
|
|
|
|
await expect(addLinkToSection(app, "MISSING.md", "Towns", "[[X]]")).resolves.toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inserts link before the --- separator, not after it", async () => {
|
|
|
|
|
// Template structure: ### Towns\n\n---\n\n### Dungeons
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Towns\n\n---\n\n### Dungeons\n\n");
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Millhaven]]");
|
|
|
|
|
const content = getContent();
|
|
|
|
|
const townsIdx = content.indexOf("### Towns");
|
|
|
|
|
const linkIdx = content.indexOf("[[Millhaven]]");
|
|
|
|
|
const hrIdx = content.indexOf("---");
|
|
|
|
|
const dungeonsIdx = content.indexOf("### Dungeons");
|
|
|
|
|
// Link must appear between the heading and the --- separator
|
|
|
|
|
expect(linkIdx).toBeGreaterThan(townsIdx);
|
|
|
|
|
expect(linkIdx).toBeLessThan(hrIdx);
|
|
|
|
|
// --- and ### Dungeons must remain after the link
|
|
|
|
|
expect(hrIdx).toBeLessThan(dungeonsIdx);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inserts second link before the --- separator when section already has a link", async () => {
|
|
|
|
|
const { app, getContent } = makeApp(
|
|
|
|
|
"hex.md",
|
|
|
|
|
"### Towns\n\n[[Riverdale]]\n\n---\n\n### Dungeons\n\n",
|
|
|
|
|
);
|
|
|
|
|
await addLinkToSection(app, "hex.md", "Towns", "[[Millhaven]]");
|
|
|
|
|
const content = getContent();
|
|
|
|
|
const hrIdx = content.indexOf("---");
|
|
|
|
|
const millIdx = content.indexOf("[[Millhaven]]");
|
|
|
|
|
const riverIdx = content.indexOf("[[Riverdale]]");
|
|
|
|
|
expect(riverIdx).toBeLessThan(hrIdx);
|
|
|
|
|
expect(millIdx).toBeLessThan(hrIdx);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── removeLinkFromSection ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("removeLinkFromSection", () => {
|
|
|
|
|
it("removes an existing link from a section", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Towns\n\n[[Riverdale]]\n[[Millhaven]]\n");
|
|
|
|
|
await removeLinkFromSection(app, "hex.md", "Towns", "Riverdale");
|
|
|
|
|
expect(getContent()).not.toContain("[[Riverdale]]");
|
|
|
|
|
expect(getContent()).toContain("[[Millhaven]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is a no-op when the link is not present", async () => {
|
|
|
|
|
const original = "### Towns\n\n[[Millhaven]]\n";
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", original);
|
|
|
|
|
await removeLinkFromSection(app, "hex.md", "Towns", "Missing");
|
|
|
|
|
expect(getContent()).toBe(original);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is a no-op when the section does not exist", async () => {
|
|
|
|
|
const original = "### Dungeons\n\n[[Cave]]\n";
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", original);
|
|
|
|
|
await removeLinkFromSection(app, "hex.md", "Towns", "Cave");
|
|
|
|
|
expect(getContent()).toBe(original);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not remove a link from a different section", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Towns\n\n[[Village]]\n\n### Dungeons\n\n[[Village]]\n");
|
|
|
|
|
await removeLinkFromSection(app, "hex.md", "Towns", "Village");
|
|
|
|
|
// Link in Dungeons should survive
|
|
|
|
|
expect(getContent()).toContain("### Dungeons");
|
|
|
|
|
const dungeonSection = getContent().split("### Dungeons")[1];
|
|
|
|
|
expect(dungeonSection).toContain("[[Village]]");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── getLinksInSection ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("getLinksInSection", () => {
|
|
|
|
|
it("returns all wiki-links in the section", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Towns\n\n[[Riverdale]]\n[[Millhaven]]\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual(["Riverdale", "Millhaven"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty array when section has no links", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Towns\n\nJust text, no links.\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty array when section does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Dungeons\n\n[[Cave]]\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty array when file does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "");
|
|
|
|
|
const links = await getLinksInSection(app, "MISSING.md", "Towns");
|
|
|
|
|
expect(links).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("handles links with display text (pipe syntax)", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Towns\n\n[[path/to/Town|Town Name]]\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual(["path/to/Town"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stops at the next heading", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Towns\n\n[[A]]\n\n### Dungeons\n\n[[B]]\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual(["A"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stops at a horizontal rule (--- separator)", async () => {
|
|
|
|
|
// Matches default hex template structure where sections are separated by ---
|
|
|
|
|
const { app } = makeApp("hex.md", "### Towns\n\n[[A]]\n\n---\n\n### Dungeons\n\n[[B]]\n");
|
|
|
|
|
const links = await getLinksInSection(app, "hex.md", "Towns");
|
|
|
|
|
expect(links).toEqual(["A"]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── getSectionContent ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("getSectionContent", () => {
|
|
|
|
|
it("returns the trimmed body of a section", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Description\n\nA misty valley.\n");
|
|
|
|
|
const content = await getSectionContent(app, "hex.md", "Description");
|
|
|
|
|
expect(content).toBe("A misty valley.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty string when section does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Other\n\nSomething\n");
|
|
|
|
|
const content = await getSectionContent(app, "hex.md", "Description");
|
|
|
|
|
expect(content).toBe("");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty string when file does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "");
|
|
|
|
|
const content = await getSectionContent(app, "MISSING.md", "Description");
|
|
|
|
|
expect(content).toBe("");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stops at the next heading", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Description\n\nLine one.\n\n### Notes\n\nLine two.\n");
|
|
|
|
|
const content = await getSectionContent(app, "hex.md", "Description");
|
|
|
|
|
expect(content).toBe("Line one.");
|
|
|
|
|
expect(content).not.toContain("Line two");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stops at a horizontal rule", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### Description\n\nBefore rule.\n\n---\n\nAfter rule.\n");
|
|
|
|
|
const content = await getSectionContent(app, "hex.md", "Description");
|
|
|
|
|
expect(content).toBe("Before rule.");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── getAllSectionData ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("getAllSectionData", () => {
|
|
|
|
|
it("returns empty maps for a file with no sections", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "Just prose, no headings.");
|
|
|
|
|
const { text, links } = await getAllSectionData(app, "hex.md");
|
|
|
|
|
expect(text.size).toBe(0);
|
|
|
|
|
expect(links.size).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns empty maps when file does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "");
|
|
|
|
|
const { text, links } = await getAllSectionData(app, "MISSING.md");
|
|
|
|
|
expect(text.size).toBe(0);
|
|
|
|
|
expect(links.size).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("captures text and links from multiple sections", async () => {
|
|
|
|
|
const content = [
|
|
|
|
|
"### Description",
|
|
|
|
|
"",
|
|
|
|
|
"Foggy mountains.",
|
|
|
|
|
"",
|
|
|
|
|
"### Towns",
|
|
|
|
|
"",
|
|
|
|
|
"[[Riverdale]]",
|
|
|
|
|
"[[Millhaven]]",
|
|
|
|
|
].join("\n");
|
|
|
|
|
const { app } = makeApp("hex.md", content);
|
|
|
|
|
const { text, links } = await getAllSectionData(app, "hex.md");
|
|
|
|
|
|
|
|
|
|
expect(text.get("description")).toBe("Foggy mountains.");
|
|
|
|
|
expect(links.get("towns")).toEqual(["Riverdale", "Millhaven"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("uses lowercase keys for section names", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "### My Section\n\nHello.\n");
|
|
|
|
|
const { text } = await getAllSectionData(app, "hex.md");
|
|
|
|
|
expect(text.has("my section")).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── setSectionContent ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("setSectionContent", () => {
|
|
|
|
|
it("replaces the body of an existing section", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Description\n\nOld text.\n");
|
|
|
|
|
await setSectionContent(app, "hex.md", "Description", "New text.");
|
|
|
|
|
expect(getContent()).toContain("New text.");
|
|
|
|
|
expect(getContent()).not.toContain("Old text.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("creates the section when it does not exist", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "Some content.");
|
|
|
|
|
await setSectionContent(app, "hex.md", "Notes", "My note.");
|
|
|
|
|
expect(getContent()).toContain("### Notes");
|
|
|
|
|
expect(getContent()).toContain("My note.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("clears section body when new text is empty", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", "### Description\n\nOld text.\n");
|
|
|
|
|
await setSectionContent(app, "hex.md", "Description", "");
|
|
|
|
|
expect(getContent()).not.toContain("Old text.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not create a section for empty new text", async () => {
|
|
|
|
|
const original = "No sections here.";
|
|
|
|
|
const { app, getContent } = makeApp("hex.md", original);
|
|
|
|
|
await setSectionContent(app, "hex.md", "Notes", "");
|
|
|
|
|
expect(getContent()).toBe(original);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not affect adjacent sections", async () => {
|
|
|
|
|
const { app, getContent } = makeApp("hex.md",
|
|
|
|
|
"### Description\n\nOld.\n\n### Towns\n\n[[A]]\n",
|
|
|
|
|
);
|
|
|
|
|
await setSectionContent(app, "hex.md", "Description", "Updated.");
|
|
|
|
|
expect(getContent()).toContain("### Towns");
|
|
|
|
|
expect(getContent()).toContain("[[A]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is a no-op when file does not exist", async () => {
|
|
|
|
|
const { app } = makeApp("hex.md", "");
|
|
|
|
|
await expect(setSectionContent(app, "MISSING.md", "Description", "text")).resolves.toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── addBacklinkToFile ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Build an app with two independent in-memory files for backlink tests. */
|
|
|
|
|
function makeAppForBacklink(
|
|
|
|
|
hexPath: string,
|
|
|
|
|
hexContent: string,
|
|
|
|
|
targetPath: string,
|
|
|
|
|
targetContent: string,
|
|
|
|
|
/** If provided, the target's metadata cache will contain a link to this resolved path. */
|
|
|
|
|
existingBacklinkToHex = false,
|
|
|
|
|
) {
|
|
|
|
|
const hexFile = Object.create(TFile.prototype) as TFile;
|
|
|
|
|
hexFile.path = hexPath;
|
|
|
|
|
|
|
|
|
|
const targetFile = Object.create(TFile.prototype) as TFile;
|
|
|
|
|
targetFile.path = targetPath;
|
|
|
|
|
|
|
|
|
|
const contents: Record<string, string> = {
|
|
|
|
|
[hexPath]: hexContent,
|
|
|
|
|
[targetPath]: targetContent,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const app = {
|
|
|
|
|
vault: {
|
|
|
|
|
getAbstractFileByPath: (p: string) => {
|
|
|
|
|
if (p === hexPath) return hexFile;
|
|
|
|
|
if (p === targetPath) return targetFile;
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
read: mock.fn(async (f: TFile) => contents[f.path] ?? ""),
|
|
|
|
|
process: mock.fn(async (f: TFile, fn: (s: string) => string) => { contents[f.path] = fn(contents[f.path] ?? ""); return contents[f.path]; }),
|
|
|
|
|
},
|
|
|
|
|
metadataCache: {
|
|
|
|
|
getFileCache: mock.fn((f: TFile) => {
|
|
|
|
|
if (f === targetFile && existingBacklinkToHex) {
|
|
|
|
|
return { links: [{ link: hexPath }] };
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}),
|
|
|
|
|
getFirstLinkpathDest: mock.fn((_link: string, _src: string) =>
|
|
|
|
|
existingBacklinkToHex ? hexFile : null,
|
|
|
|
|
),
|
|
|
|
|
fileToLinktext: mock.fn((f: TFile, _src: string) => f.path.replace(/\.md$/, "")),
|
|
|
|
|
},
|
|
|
|
|
} as unknown as import("obsidian").App;
|
|
|
|
|
|
|
|
|
|
return { app, getContent: (path: string) => contents[path] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("addBacklinkToFile", () => {
|
|
|
|
|
it("is a no-op when hexFile does not exist", async () => {
|
|
|
|
|
const { app, getContent } = makeAppForBacklink("hex/1_1.md", "", "notes/town.md", "Town content.");
|
|
|
|
|
await addBacklinkToFile(app, "notes/town.md", "MISSING.md");
|
|
|
|
|
expect(getContent("notes/town.md")).toBe("Town content.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is a no-op when targetFile does not exist", async () => {
|
|
|
|
|
const { app } = makeAppForBacklink("hex/1_1.md", "", "notes/town.md", "Town content.");
|
|
|
|
|
await expect(addBacklinkToFile(app, "MISSING.md", "hex/1_1.md")).resolves.toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("appends a wiki-link to the target file", async () => {
|
|
|
|
|
const { app, getContent } = makeAppForBacklink("hex/1_1.md", "", "notes/town.md", "Town content.");
|
|
|
|
|
await addBacklinkToFile(app, "notes/town.md", "hex/1_1.md");
|
|
|
|
|
expect(getContent("notes/town.md")).toContain("[[hex/1_1]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("separates the link from existing content with a blank line", async () => {
|
|
|
|
|
const { app, getContent } = makeAppForBacklink("hex/1_1.md", "", "notes/town.md", "Town content.");
|
|
|
|
|
await addBacklinkToFile(app, "notes/town.md", "hex/1_1.md");
|
|
|
|
|
expect(getContent("notes/town.md")).toContain("Town content.\n\n[[hex/1_1]]");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not add a blank line prefix when target is empty", async () => {
|
|
|
|
|
const { app, getContent } = makeAppForBacklink("hex/1_1.md", "", "notes/town.md", "");
|
|
|
|
|
await addBacklinkToFile(app, "notes/town.md", "hex/1_1.md");
|
|
|
|
|
expect(getContent("notes/town.md")).toBe("[[hex/1_1]]\n");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not append when the target already links to the hex (via cache)", async () => {
|
|
|
|
|
const { app, getContent } = makeAppForBacklink(
|
|
|
|
|
"hex/1_1.md", "", "notes/town.md", "[[hex/1_1]]\n",
|
|
|
|
|
true, // existingBacklinkToHex
|
|
|
|
|
);
|
|
|
|
|
await addBacklinkToFile(app, "notes/town.md", "hex/1_1.md");
|
|
|
|
|
// process should never have been called
|
|
|
|
|
assert.strictEqual(app.vault.process.mock.callCount(), 0, "process should not have been called");
|
|
|
|
|
});
|
|
|
|
|
});
|