Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .eleventy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const moment = require('moment');
const crs = require('./crs/crs.js');

moment.locale('en');

Expand All @@ -11,9 +12,12 @@ module.exports = function (eleventyConfig) {

// https://github.com/victornpb/eleventy-plugin-page-assets
eleventyConfig.addPlugin(pageAssetsPlugin, {
mode: "parse",
assetsMatching: "*.png|*.PNG|*.jpg|*.JPG|*.gif|*.GIF",
postsMatching: "**/*.md",
mode: "directory",
assetsMatching: "*.png|*.PNG|*.jpg|*.JPG|*.gif|*.GIF|*.cr|*.nr|*.txt",
postsMatching: "**/*.md",
hashAssets: false,
recursive: true,
silent: true,
});

eleventyConfig.addFilter('dateIso', date => {
Expand All @@ -28,6 +32,12 @@ module.exports = function (eleventyConfig) {

// Folders to copy to output folder
eleventyConfig.addPassthroughCopy("css");

crs(eleventyConfig);

// all reports go here
eleventyConfig.addPassthroughCopy("reports");

};

module.exports.config = {
Expand Down
3 changes: 3 additions & 0 deletions .eleventyignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# We don't want Eleventy to include the README.md as a website content file
README.md

# template directory: help for authors
# template/

# Unit tests should be ignored by Eleventy
tests/
11 changes: 9 additions & 2 deletions _includes/base-layout.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Eressea Beispielpartie</title>
<title>Eressea Beispielpartie - {{title}}</title>

<link rel="stylesheet" href="{{ '/css/site.css' }}">
{% if content.indexOf('crs-requires-css') !== -1 %}
<link rel="stylesheet" href="{{ '/css/crs.css' }}">
{% endif %}
{% if content.indexOf('crs-requires-js') !== -1 %}
Comment on lines +9 to +12
Copy link

Copilot AI Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional asset loading uses string search on content, which could have false positives if the marker class appears in text content. Consider using a more robust approach like setting template variables in the shortcodes.

Suggested change
{% if content.indexOf('crs-requires-css') !== -1 %}
<link rel="stylesheet" href="{{ '/css/crs.css' }}">
{% endif %}
{% if content.indexOf('crs-requires-js') !== -1 %}
{% if crs_requires_css %}
<link rel="stylesheet" href="{{ '/css/crs.css' }}">
{% endif %}
{% if crs_requires_js %}

Copilot uses AI. Check for mistakes.
<script src="{{ '/crs/crs-passthrough.js' }}" defer></script>
{% endif %}

<link rel="stylesheet" href="{{ '/css/site.css' | url }}">
<!-- link href="https://fonts.googleapis.com/css?family=Roboto+Slab:700|Roboto&display=fallback" rel="stylesheet" -->
</head>
<body>
Expand Down
185 changes: 185 additions & 0 deletions crs/crs-passthrough.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// === Helpers ==============================================================
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

function unitShort(unit) {
let uhtml = `<b>${escapeHtml(unit.name)} (${escapeHtml(unit.id)})</b>` + (unit.factionName ? `, <span class="faction">${escapeHtml(unit.factionName)} (${escapeHtml(unit.faction)})</span>` : '');
if (unit.tags && unit.tags.Anzahl) {
uhtml += `, ${escapeHtml(unit.tags.Anzahl)}`;
}
if (unit.tags && unit.tags.Typ) {
uhtml += ` ${escapeHtml(unit.tags.Typ)}`;
}
return uhtml;
}

function parseRegionData(regionUse) {
const link = regionUse.closest('.cr-region-link') || regionUse;
if (!link) return null;
if (link._crRegionData) return link._crRegionData; // cache
const attr = link.getAttribute('data-region');
if (!attr) return null;
try {
link._crRegionData = JSON.parse(decodeURIComponent(attr));
return link._crRegionData;
} catch (e) {
link._crRegionDataError = e;
return null;
}
}

function parseUnitsData(regionUse) {
const parent = regionUse.parentNode;
if (!parent) return [];
const unitsUse = parent.querySelector('use[data-units]');
if (!unitsUse) return [];
if (unitsUse._crUnitsData) return unitsUse._crUnitsData;
const attr = unitsUse.getAttribute('data-units');
if (!attr) return [];
try {
unitsUse._crUnitsData = JSON.parse(decodeURIComponent(attr));
return unitsUse._crUnitsData;
} catch (e) {
unitsUse._crUnitsDataError = e;
return [];
}
}

function buildRegionHtml(regionData) {
if (!regionData || !regionData.tags) return '';
let html = `<b>${escapeHtml(regionData.tags.Terrain || '')} ${escapeHtml(regionData.tags.Name || '')}</b> (${escapeHtml(regionData.x)}, ${escapeHtml(regionData.y)})`;
const entries = Object.entries(regionData.tags).filter(([k]) => !['Name', 'id', 'Terrain'].includes(k));
if (entries.length) {
html += '<div class="region_tags">' + entries.map(([k, v]) => `<div><i>${escapeHtml(k)}</i>: ${escapeHtml(v)}</div>`).join('') + '</div>';
}
return html;
}

function buildUnitListHtml(units, regionDomId, targetId) {
if (!units || !units.length) return '';
let html = '<div class="unit_block"><b>Units:</b><ul>';
for (const u of units) {
const cls = u.isOwner ? 'owner-unit' : 'other-unit';
html += `<li class="${cls} cr-unit-link" data-unit-id="${escapeHtml(u.id)}" data-region-id="${escapeHtml(regionDomId)}" data-region-target="${escapeHtml(targetId)}">` + unitShort(u) + '</li>';
}
html += '</ul></div>';
return html;
}

function buildUnitDetailHtml(unit) {
if (!unit) return '';
let uhtml = '<div class="unit_detail">' + unitShort(unit);
if (unit.skills && Object.keys(unit.skills).length) {
uhtml += '<div class="skills">' + Object.entries(unit.skills).map(([sk, val]) => `<span>${escapeHtml(sk)} ${escapeHtml(val)}</span>`).join(', ') + '</div>';
}
if (unit.items && Object.keys(unit.items).length) {
uhtml += '<div class="items">' + Object.entries(unit.items).map(([item, amount]) => `<span>${escapeHtml(amount)} ${escapeHtml(item)}</span>`).join(', ') + '</div>';
}
const omit = ['Name', 'id', 'Partei', 'Anzahl', 'Typ'];
const rest = Object.entries(unit.tags || {}).filter(([k]) => !omit.includes(k));
if (rest.length) {
uhtml += '<div class="unit_tags">' + rest.map(([k, v]) => `<div><i>${escapeHtml(k)}</i>: ${escapeHtml(v)}</div>`).join('') + '</div>';
}
uhtml += '</div>';
return uhtml;
}

function buildUnitCommandsHtml(unit) {
if (!unit) return '';
const commands = (unit.commands || []).join('\n');
return '<textarea readonly style="width:100%;min-height:8em;">' + escapeHtml(commands) + '</textarea>';
}

function updateUnitPanels(regionUse, units, unitId) {
if (!unitId) return; // nothing to do
const unit = units.find(u => u.id === unitId);
if (!unit) return;
const cridVal = (regionUse.getAttribute('data-crid') || '');
const unitDetails = document.getElementById('udetails_' + cridVal);
const unitCommands = document.getElementById('ucommands_' + cridVal);
if (unitDetails) unitDetails.innerHTML = buildUnitDetailHtml(unit);
if (unitCommands) unitCommands.innerHTML = buildUnitCommandsHtml(unit);
}

// === Orchestrator =========================================================
function showDescription(event, targetDivId, regionDomId, unitId) {
if (event) event.preventDefault();
if (!regionDomId) return false;
const regionTarget = document.getElementById(targetDivId);
if (!regionTarget) return false;
const regionUse = document.getElementById(regionDomId);
if (!regionUse) return false;

const regionData = parseRegionData(regionUse);
const units = parseUnitsData(regionUse);

// Update side panels for a specific unit (details + commands) first so unit view stays in sync
if (unitId) updateUnitPanels(regionUse, units, unitId);

let html = '';
html += buildRegionHtml(regionData);
html += buildUnitListHtml(units, regionDomId, targetDivId);

regionTarget.innerHTML = html || 'No details';
regionTarget.style.display = 'block';
return false;
}

function showTooltip(event, id, text) {
const tooltip = document.getElementById(id);
tooltip.style.display = 'block';
tooltip.innerHTML = text;
tooltip.style.display = 'block';
const x = event.clientX + 10;
const y = event.clientY - 10;
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
}

function hideTooltip(id) {
const tooltip = document.getElementById(id);
tooltip.style.display = 'none';
}

// TODO: Accessibility: add keyboard listeners (Enter/Space) for .cr-region-link and .cr-unit-link
// and ARIA roles (button/list/listitem) in a subsequent enhancement.

(function initCRMapBindings() {
if (window.__crMapBound) return; // avoid rebinding on partial reloads
window.__crMapBound = true;
document.addEventListener('mousemove', function (e) {
const target = e.target?.closest?.('.cr-region-link') || null;
if (target && target.dataset.tooltipId && target.dataset.tooltip) {
showTooltip(e, target.dataset.tooltipId, target.dataset.tooltip);
}
}, true);
document.addEventListener('mouseout', function (e) {
const rel = e.relatedTarget;
const link = e.target?.closest?.('.cr-region-link') || null;
if (link && (!rel || !rel.closest('.cr-region-link')) && link.dataset.tooltipId) {
hideTooltip(link.dataset.tooltipId);
}
}, true);
document.addEventListener('click', function (e) {
const regionLink = e.target?.closest?.('.cr-region-link') || null;
if (regionLink && regionLink.dataset.regionId && regionLink.dataset.regionTarget) {
showDescription(e, regionLink.dataset.regionTarget, regionLink.dataset.regionId);
e.preventDefault();
return;
}
const unitLink = e.target.closest('.cr-unit-link');
if (unitLink) {
const regionId = unitLink.dataset.regionId;
const rtarget = unitLink.dataset.regionTarget;
const uid = unitLink.dataset.unitId;
showDescription(e, rtarget, regionId, uid);
e.preventDefault();
}
});
})();
Loading