3 Commits

Author SHA1 Message Date
6faab5aa19 fix 2
All checks were successful
Build and publish CMBA rulebooks (Gitea) / build-release (push) Successful in 6s
2026-01-16 16:02:54 -06:00
900305df1d fix
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 5s
2026-01-16 16:00:28 -06:00
ab397c18dd cleanup, migrate to mkdocs
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 49s
2026-01-16 15:56:03 -06:00
11 changed files with 85 additions and 334 deletions

View File

@@ -8,128 +8,22 @@ on:
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install Dependencies
run: pip install -r mkdocs/requirements.txt
- name: Build Docs
run: |
sudo apt-get update
# texlive-latex-recommended \
# texlive-fonts-recommended \
# texlive-latex-extra \
sudo apt-get install -y pandoc \
jq \
curl
- name: Build pandoc after-body include from .js files
run: |
set -euo pipefail
mkdir -p build
out="build/after-body.html"
: > "$out" # truncate
# Ensure deterministic order
shopt -s nullglob
files=(js/*.js)
if [ ${#files[@]} -eq 0 ]; then
echo "No JS files found in js/; generating empty after-body.html"
exit 0
fi
for f in "${files[@]}"; do
{
echo "<script>"
echo "$f"
echo "</script>"
} >> "$out"
done
- name: Build artifacts
run: |
set -euo pipefail
mkdir -p dist
for doc in *.md; do
# Skip README.md
[[ "$doc" == "README.md" ]] && continue
basename="${doc%.md}"
# Mobile-friendly HTML with TOC + serif styling
pandoc "${doc}" \
--toc \
--standalone \
--number-sections \
--toc-depth=2 \
--lua-filter pandoc-filters/shift-numbering.lua \
--metadata-file metadata.yml \
--embed-resources \
--metadata title="CMBA ${basename}" \
--include-after-body=build/after-body.html \
--css styles/style.css \
-o "dist/${basename}.html"
# PDF
# pandoc "${doc}" \
# --metadata title="CMBA ${doc}" \
# -o "dist/${doc}.pdf"
# Include source markdown for transparency
cp ${doc} dist/
done
- name: Create or update Gitea Release and upload assets
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
# Gitea provides these variables in Actions:
REPO: ${{ gitea.repository }} # "owner/repo"
TAG: ${{ gitea.ref_name }} # e.g. "v2026.0"
run: |
set -euo pipefail
OWNER="$(echo "$REPO" | cut -d/ -f1)"
NAME="$(echo "$REPO" | cut -d/ -f2)"
API="${GITHUB_API_URL}"
AUTH="Authorization: token ${GITEA_TOKEN}"
echo "Repo: $OWNER/$NAME"
echo "Tag: $TAG"
echo "API: $API"
# Check if release exists for this tag
existing_release_json="$(curl -sS -H "$AUTH" \
"$API/repos/$OWNER/$NAME/releases/tags/$TAG" || true)"
if echo "$existing_release_json" | jq -e '.id' >/dev/null 2>&1; then
RELEASE_ID="$(echo "$existing_release_json" | jq -r '.id')"
echo "Release exists (id=$RELEASE_ID)."
else
echo "Creating release for tag $TAG..."
create_json="$(curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
"$API/repos/$OWNER/$NAME/releases" \
-d "$(jq -n --arg tag "$TAG" --arg name "$TAG" \
'{tag_name:$tag, name:$name, draft:false, prerelease:false, body:"CMBA rulebooks release."}')")"
RELEASE_ID="$(echo "$create_json" | jq -r '.id')"
echo "Created release (id=$RELEASE_ID)."
fi
# Upload each file as an asset. If an asset already exists with same name, delete then re-upload.
assets_json="$(curl -sS -H "$AUTH" "$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets" || echo '[]')"
for f in dist/*; do
filename="$(basename "$f")"
existing_asset_id="$(echo "$assets_json" | jq -r --arg n "$filename" '.[] | select(.name==$n) | .id' | head -n1 || true)"
if [ -n "${existing_asset_id:-}" ] && [ "$existing_asset_id" != "null" ]; then
echo "Deleting existing asset: $filename (id=$existing_asset_id)"
curl -sS -X DELETE -H "$AUTH" \
"$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets/$existing_asset_id" >/dev/null
fi
echo "Uploading $filename"
curl -sS --fail -X POST -H "$AUTH" -H "Accept: application/json" -F "attachment=@$f" "$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets?name=$filename" >/dev/null
done
echo "Done. Release page: ${GITEA_BASE_URL}/${OWNER}/${NAME}/releases/tag/${TAG}"
mkdir $RUNNER_TEMP/dist
mkdocs build -f mkdocs/mkdocs.yml -d $RUNNER_TEMP/${{ github.ref_name }}
zip -r $RUNNER_TEMP/dist/${{ github.ref_name }}.zip $RUNNER_TEMP/${{ github.ref_name }}
- name: Release
if: ${{ !env.ACT }}
uses: softprops/action-gh-release@v2
with:
files: |
$RUNNER_TEMP/dist/*.zip

5
.gitignore vendored
View File

@@ -7,8 +7,13 @@
/dataSources/
/dataSources.local.xml
# Vitrtual Environments
.venv/
venv/
.idea/
*.bbprojectd
.vscode
# Backups created by aspell
*.md.bak*

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,5 +1,4 @@
# By-Laws {.unnumbered}
# Bylaws
## Definition; Amendments
These By-Laws shall stand as the official rules of the CMBA.
All Association members shall abide by and are subject to, without exception, all rules and regulations as outlined in the Constitution and By-Laws as well as the authority of the Association President, in accordance with [Subjectivity](#subjectivity)

View File

@@ -1,185 +0,0 @@
(() => {
// Inject Quote buttons into Pandoc-style headings and copy a section-range quote to clipboard.
//
// Assumptions (matches your sample):
// - Headings are h1/h2 with stable ids (anchors).
// - A "section" is: the heading + all following sibling elements until the next heading
// of the same or higher level (H2 stops at next H2 or H1; H1 stops at next H1).
//
// Primary target: GroupMe. We optimize text/plain to be readable and "quote-like".
// We still provide text/html for rich paste targets (Docs/Email).
//
// No dependencies.
const HEADING_SELECTOR = "h1[id], h2[id]"; // change to "h2[id]" if you only want section-level
document.addEventListener("DOMContentLoaded", () => {
injectQuoteButtons();
});
function injectQuoteButtons() {
document.querySelectorAll(HEADING_SELECTOR).forEach((heading) => {
// Avoid double-injection
if (heading.querySelector(":scope > .quote-btn")) return;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "quote-btn";
btn.textContent = "";
btn.innerHTML = `
<span class="quote-icon" aria-hidden="true"></span>
`;
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
onQuoteClick(btn, heading);
});
// Add a little separation from heading text
heading.appendChild(document.createTextNode(" "));
heading.appendChild(btn);
});
}
async function onQuoteClick(btn, heading) {
const originalText = btn.textContent;
try {
btn.textContent = "Copying…";
btn.disabled = true;
await copyQuotedSection(heading);
btn.textContent = "Copied";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
}, 800);
} catch (err) {
console.error(err);
btn.textContent = "Failed";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
}, 1200);
}
}
async function copyQuotedSection(heading) {
if (!heading.id) throw new Error("Heading missing id.");
const level = heading.tagName; // H1 or H2
// Clone heading + subsequent siblings for the quote range
const nodes = [];
nodes.push(heading.cloneNode(true));
let el = heading.nextElementSibling;
while (el) {
if (isBoundary(level, el)) break;
nodes.push(el.cloneNode(true));
el = el.nextElementSibling;
}
// Remove injected button from cloned heading (so it doesn't show up in pasted content)
nodes[0].querySelector(".quote-btn")?.remove();
const container = document.createElement("div");
nodes.forEach((n) => container.appendChild(n));
const href = buildAnchorUrl(heading.id);
// HTML (for rich paste targets)
const html = buildHtmlQuote(container.innerHTML, href);
// Plain text (optimized for GroupMe)
const plain = buildGroupMePlainQuote(container, href);
await writeClipboardMultiFormat({ html, plain, url: href });
}
function isBoundary(level, el) {
// Stop at next heading of same or higher rank.
// H2 stops at next H1 or H2.
// H1 stops at next H1.
if (level === "H1") return el.tagName === "H1";
if (level === "H2") return el.tagName === "H1" || el.tagName === "H2";
return false;
}
function buildAnchorUrl(id) {
const url = new URL(window.location.href);
url.hash = id;
return url.toString();
}
function buildHtmlQuote(innerHtml, href) {
const safeHref = escapeHtmlAttr(href);
const safeText = escapeHtml(href);
return `
<blockquote>
${innerHtml}
<p><a href="${safeHref}">${safeText}</a></p>
</blockquote>
`.trim();
}
function buildGroupMePlainQuote(container, href) {
// Create a readable quote-like block:
// - Prefix lines with "> " to visually indicate quoting (works even if not rendered specially)
// - Include Source link
const text = normalizePlainText(container);
const quoted = text
.split("\n")
.map((line) => (line.trim().length ? `> ${line}` : `>`))
.join("\n");
return `${quoted}\n> \n> Source: ${href}`;
}
function normalizePlainText(container) {
// Use innerText to approximate what the user sees (captures list numbering reasonably in many browsers).
// Then normalize spacing.
return (container.innerText || "")
.replace(/\r\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
async function writeClipboardMultiFormat({ html, plain, url }) {
// Best path: multi-format clipboard write (Chromium/Edge, etc.)
if (navigator.clipboard?.write) {
const item = new ClipboardItem({
"text/html": new Blob([html], { type: "text/html" }),
"text/plain": new Blob([plain], { type: "text/plain" }),
// Best-effort URL type; some paste targets prefer this
"text/uri-list": new Blob([url], { type: "text/uri-list" }),
});
await navigator.clipboard.write([item]);
return;
}
// Fallback: plain text only
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(plain);
return;
}
throw new Error("Clipboard API unavailable.");
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function escapeHtmlAttr(s) {
return escapeHtml(s);
}
})();

View File

@@ -1,5 +0,0 @@
---
title: Chicago Metropolitan Baseball Association
subtitle: Constitution and By-Laws
date: 2024-06-06
---

3
mkdocs/index.md Normal file
View File

@@ -0,0 +1,3 @@
# CMBA Rulebooks
Use the navigation to view the documents.

48
mkdocs/mkdocs.yml Normal file
View File

@@ -0,0 +1,48 @@
site_name: CMBA Rulebook
docs_dir: ..
site_dir: ./dist
# Prevent accidental publishing of repo/CI/dev clutter
exclude_docs: |
.github/
.git/
.venv/
venv/
"*.sh"
"*.py"
"*.yml"
"*.yaml"
"README.md"
theme:
name: material
palette:
scheme: default
primary: blue
accent: red
logo: ../assets/cmba-logo.svg
favicon: ../assets/favicon.png
features:
- navigation.instant
- navigation.tracking
- navigation.top
- toc.integrate
- search.highlight
- search.suggest
extra_css:
- mkdocs/styles/extra.css
markdown_extensions:
- toc:
permalink: true
- admonition:
# - tables
nav:
- Home: mkdocs/index.md
- Constitution: cmba-constitution.md
- Bylaws: cmba-bylaws.md
plugins:
- search
- enumerate-headings:
toc_depth: 2
exclude:
- mkdocs/index.md

2
mkdocs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
mkdocs-enumerate-headings-plugin==0.6
mkdocs-material==9.7

10
mkdocs/styles/extra.css Normal file
View File

@@ -0,0 +1,10 @@
@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&display=swap');
:root {
--md-text-font: "Merriweather"
}
.md-nav__title {
font-family: "Nunito Sans";
}

View File

@@ -1,20 +0,0 @@
-- Based on https://github.com/jgm/pandoc/issues/5071#issuecomment-856918980
-- For LaTeX documents, shift all headings down by one.
-- Make Level 1 unnumbered, or remove it if it contains the "hidden" class. (`# Title {.hidden}`)
function Header(el)
if el.level == 1 then
--check for hidden class. (Why didn't pandoc make this a set? `{class_name = true}`)
for i, v in ipairs(el.classes) do
if v == "hidden" then
--The empty list means remove the element
return {}
end
end
-- this may mean that there are two unnumbered classes, but that doesn't matter.
el.classes[#el.classes + 1] = "unnumbered"
else
-- all headings > 1 are shifted down so that you don't end up with 0.x.y headings.
el.level = el.level - 1
end
return el
end