cleanup, migrate to mkdocs
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 49s
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 49s
This commit is contained in:
137
.github/workflows/release-docs.yml
vendored
137
.github/workflows/release-docs.yml
vendored
@@ -8,128 +8,21 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-release:
|
build-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
- name: Install dependencies
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
- run: pip install -r mkdocs/requirements.txt
|
||||||
|
- run: mkdocs build -f mkdocs/mkdocs.yml -d $RUNNER_TEMP/${{ github.ref_name }}
|
||||||
|
# - run: mkdocs gh-deploy --force
|
||||||
|
- name: Zip Folder
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
zip -r dist/{{ github.ref_name }}.zip $RUNNER_TEMP/${{ github.ref_name }}
|
||||||
|
- name: Release
|
||||||
# texlive-latex-recommended \
|
if: ${{ !env.ACT }}
|
||||||
# texlive-fonts-recommended \
|
uses: softprops/action-gh-release@v2
|
||||||
# texlive-latex-extra \
|
with:
|
||||||
|
files: |
|
||||||
sudo apt-get install -y pandoc \
|
dist/*.zip
|
||||||
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}"
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,8 +7,13 @@
|
|||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
|
||||||
|
# Vitrtual Environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
*.bbprojectd
|
*.bbprojectd
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Backups created by aspell
|
# Backups created by aspell
|
||||||
*.md.bak*
|
*.md.bak*
|
||||||
|
|||||||
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -1,5 +1,4 @@
|
|||||||
# By-Laws {.unnumbered}
|
# Bylaws
|
||||||
|
|
||||||
## Definition; Amendments
|
## Definition; Amendments
|
||||||
These By-Laws shall stand as the official rules of the CMBA.
|
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)
|
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)
|
||||||
|
|||||||
185
js/quote.js
185
js/quote.js
@@ -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("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtmlAttr(s) {
|
|
||||||
return escapeHtml(s);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: Chicago Metropolitan Baseball Association
|
|
||||||
subtitle: Constitution and By-Laws
|
|
||||||
date: 2024-06-06
|
|
||||||
---
|
|
||||||
3
mkdocs/index.md
Normal file
3
mkdocs/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# CMBA Rulebooks
|
||||||
|
|
||||||
|
Use the navigation to view the documents.
|
||||||
48
mkdocs/mkdocs.yml
Normal file
48
mkdocs/mkdocs.yml
Normal 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
2
mkdocs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mkdocs-enumerate-headings-plugin==0.6
|
||||||
|
mkdocs-material==9.7
|
||||||
10
mkdocs/styles/extra.css
Normal file
10
mkdocs/styles/extra.css
Normal 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";
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user