mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
Adds a unified export pipeline under src/export/ with exporters for single notes,
structured hexes, maps (with reference table), random tables, and workflows
(with rolled samples). PDF rendering reuses Obsidian's MarkdownRenderer + theme
CSS; map PNG/PDF goes through a dedicated canvas renderer.
UI surface:
- "Export" tab in MapModal (PNG/PDF with overlay + size options)
- "Export" link in HexEditorModal + HexExportModal
- "Export PDF/Markdown" links in RandomTableView header
- "Export…" button in WorkflowEditorModal + WorkflowExportModal
- Commands: export current note (PDF/MD), current hex, current workflow
- File-menu items on any .md file: Export to PDF / Markdown
Other:
- New `exportFolder` setting (defaults to {worldFolder}/exports)
- hexGeometry: add hexSize / hexCenter / hexPolygonPoints / gridBoundingBox
- HexTableView + HexEditorModal: first-wins on duplicate palette names
(matches HexMapView's .find() behaviour)
- Tests: unit + integration suites for all exporters; sim-vault fixture
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import expect from "expect";
|
|
import {
|
|
parseHexCoord,
|
|
renderSectionBlock,
|
|
HEX_SECTIONS,
|
|
type HexSectionDef,
|
|
} from "../src/export/exporters/singleHex";
|
|
|
|
describe("parseHexCoord", () => {
|
|
it("parses simple positive coords", () => {
|
|
expect(parseHexCoord("5_3")).toEqual({ x: 5, y: 3 });
|
|
});
|
|
|
|
it("parses negative coords", () => {
|
|
expect(parseHexCoord("-2_-7")).toEqual({ x: -2, y: -7 });
|
|
expect(parseHexCoord("-2_3")).toEqual({ x: -2, y: 3 });
|
|
expect(parseHexCoord("5_-3")).toEqual({ x: 5, y: -3 });
|
|
});
|
|
|
|
it("returns null for non-coord basenames", () => {
|
|
expect(parseHexCoord("my-cool-hex")).toBeNull();
|
|
expect(parseHexCoord("5")).toBeNull();
|
|
expect(parseHexCoord("5_3_extra")).toBeNull();
|
|
expect(parseHexCoord("")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("renderSectionBlock", () => {
|
|
const text: HexSectionDef = {
|
|
id: "description",
|
|
title: "Description",
|
|
kind: "text",
|
|
};
|
|
const links: HexSectionDef = {
|
|
id: "towns",
|
|
title: "Towns",
|
|
kind: "links",
|
|
};
|
|
|
|
it("returns null for an empty text section", () => {
|
|
const data = { text: new Map(), links: new Map() };
|
|
expect(renderSectionBlock(text, data)).toBeNull();
|
|
});
|
|
|
|
it("returns null when the text section is whitespace only", () => {
|
|
const data = {
|
|
text: new Map([["description", " \n "]]),
|
|
links: new Map(),
|
|
};
|
|
expect(renderSectionBlock(text, data)).toBeNull();
|
|
});
|
|
|
|
it("wraps text content in a section block with heading", () => {
|
|
const data = {
|
|
text: new Map([["description", "A small fishing town."]]),
|
|
links: new Map(),
|
|
};
|
|
const out = renderSectionBlock(text, data);
|
|
expect(out).toMatch(/<section class="duckmage-hex-section">/);
|
|
expect(out).toMatch(/## Description/);
|
|
expect(out).toMatch(/A small fishing town\./);
|
|
expect(out).toMatch(/<\/section>/);
|
|
});
|
|
|
|
it("returns null for an empty link section", () => {
|
|
const data = { text: new Map(), links: new Map() };
|
|
expect(renderSectionBlock(links, data)).toBeNull();
|
|
});
|
|
|
|
it("renders link sections as a bulleted wikilink list", () => {
|
|
const data = {
|
|
text: new Map(),
|
|
links: new Map([["towns", ["Saltwatch", "Old Spire"]]]),
|
|
};
|
|
const out = renderSectionBlock(links, data);
|
|
expect(out).toMatch(/## Towns/);
|
|
expect(out).toMatch(/- \[\[Saltwatch\]\]/);
|
|
expect(out).toMatch(/- \[\[Old Spire\]\]/);
|
|
});
|
|
});
|
|
|
|
describe("HEX_SECTIONS catalogue", () => {
|
|
it("includes all standard hex sections", () => {
|
|
const ids = HEX_SECTIONS.map((s) => s.id);
|
|
// Core text sections
|
|
expect(ids).toContain("description");
|
|
expect(ids).toContain("landmark");
|
|
expect(ids).toContain("weather");
|
|
expect(ids).toContain("hooks & rumors");
|
|
expect(ids).toContain("hidden");
|
|
expect(ids).toContain("secret");
|
|
// Link sections
|
|
expect(ids).toContain("towns");
|
|
expect(ids).toContain("dungeons");
|
|
expect(ids).toContain("features");
|
|
expect(ids).toContain("quests");
|
|
expect(ids).toContain("factions");
|
|
expect(ids).toContain("encounters table");
|
|
});
|
|
|
|
it("marks Hidden and Secret as GM-only", () => {
|
|
const gmOnly = HEX_SECTIONS.filter((s) => s.gmOnly).map((s) => s.id);
|
|
expect(gmOnly).toEqual(["hidden", "secret"]);
|
|
});
|
|
|
|
it("text sections come before link sections (semantic ordering)", () => {
|
|
const firstLinksIdx = HEX_SECTIONS.findIndex((s) => s.kind === "links");
|
|
const lastTextIdx = HEX_SECTIONS.map((s, i) => ({ s, i }))
|
|
.filter(({ s }) => s.kind === "text")
|
|
.reduce((max, cur) => Math.max(max, cur.i), -1);
|
|
expect(lastTextIdx).toBeLessThan(firstLinksIdx);
|
|
});
|
|
});
|