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
229 changes: 229 additions & 0 deletions .config/hooks/authors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
"""Authors hook for the CURIOSS patterns mkdocs site.

Reads ``authors.yml`` (slug -> {name, orcid, affiliation}) and per-pattern
``authors:`` frontmatter (list of slugs), then:

* Generates a single ``authors/index.md`` page with a roster table and one
anchored section per author listing the patterns they've contributed to.
* Rewrites the ``## Contributors & Acknowledgement`` section in each pattern,
replacing the legacy bullet list with links to anchors on the authors page
while preserving any prose that follows the list.
"""

from __future__ import annotations

import re
from pathlib import Path

import yaml
from mkdocs.exceptions import PluginError
from mkdocs.structure.files import File

AUTHORS_FILE = "authors.yml"
CONTRIB_HEADING_RE = re.compile(
r"^##\s+Contributors\s*&\s*Acknowledg\w*\s*$",
re.MULTILINE,
)
BULLET_LINE_RE = re.compile(r"^\s*[-*]\s+\S")
HEADING_RE = re.compile(r"^#+\s", re.MULTILINE)


def _load_authors(docs_dir: str) -> dict:
path = Path(docs_dir) / AUTHORS_FILE
if not path.exists():
raise PluginError(f"authors hook: {AUTHORS_FILE} not found at {path}")
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
raise PluginError(
f"authors hook: {AUTHORS_FILE} must be a YAML mapping keyed by slug"
)
return data


def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
end = text.find("\n---", 3)
if end == -1:
return {}, text
fm_text = text[3:end].lstrip("\n")
body = text[end + 4 :].lstrip("\n")
try:
fm = yaml.safe_load(fm_text) or {}
except yaml.YAMLError:
return {}, text
return (fm if isinstance(fm, dict) else {}), body


def _read_authors_for_file(file: File) -> list[str]:
if not file.abs_src_path or not file.src_path.endswith(".md"):
return []
try:
text = Path(file.abs_src_path).read_text(encoding="utf-8")
except OSError:
return []
fm, _ = _parse_frontmatter(text)
raw = fm.get("authors") or []
if not isinstance(raw, list):
return []
return [s for s in raw if isinstance(s, str)]


def _pattern_title(file: File) -> str:
try:
text = Path(file.abs_src_path).read_text(encoding="utf-8")
except OSError:
return Path(file.src_path).stem
_, body = _parse_frontmatter(text)
for line in body.splitlines():
if line.startswith("# "):
return line[2:].strip()
return Path(file.src_path).stem.replace("-", " ").title()


def _render_authors_index(
authors_map: dict, by_author: dict[str, list[tuple[str, str]]]
) -> str:
sorted_authors = sorted(
authors_map.items(), key=lambda kv: kv[1].get("name", kv[0]).lower()
)

lines = [
"# Authors",
"",
"Everyone who has contributed to a CURIOSS pattern. "
"Click a name to jump to their entry below.",
"",
"| Author | Affiliation | ORCID |",
"| --- | --- | --- |",
]
for slug, record in sorted_authors:
name = record.get("name", slug)
aff = record.get("affiliation") or ""
orcid = record.get("orcid")
orcid_cell = f"[{orcid}](https://orcid.org/{orcid})" if orcid else ""
lines.append(f"| [{name}](#{slug}) | {aff} | {orcid_cell} |")

for slug, record in sorted_authors:
name = record.get("name", slug)
aff = record.get("affiliation")
orcid = record.get("orcid")
lines += ["", f"## {name} {{ #{slug} }}", ""]
if aff:
lines += [f"**Affiliation:** {aff}", ""]
if orcid:
lines += [f"**ORCID:** [{orcid}](https://orcid.org/{orcid})", ""]
lines += ["**Patterns:**", ""]
patterns = by_author.get(slug, [])
if patterns:
for src_path, title in sorted(patterns, key=lambda p: p[1].lower()):
lines.append(f"- [{title}](../{src_path})")
else:
lines.append("_No patterns yet._")
lines.append("")
return "\n".join(lines)


def _render_pattern_bullets(slugs: list[str], authors_map: dict) -> str:
bullets = []
for slug in slugs:
record = authors_map.get(slug, {})
name = record.get("name", slug)
aff = record.get("affiliation")
orcid = record.get("orcid")
line = f"- [{name}](authors/index.md#{slug})"
extras = []
if aff:
extras.append(aff)
if orcid:
extras.append(f"[ORCID](https://orcid.org/{orcid})")
if extras:
line += " — " + ", ".join(extras)
bullets.append(line)
return "\n".join(bullets)


def on_files(files, config):
authors_map = _load_authors(config["docs_dir"])

by_author: dict[str, list[tuple[str, str]]] = {slug: [] for slug in authors_map}

for f in list(files):
if not f.is_documentation_page():
continue
if f.src_path.startswith("authors/"):
continue
slugs = _read_authors_for_file(f)
if not slugs:
continue
title = _pattern_title(f)
for slug in slugs:
if slug not in authors_map:
raise PluginError(
f"authors hook: pattern '{f.src_path}' references unknown "
f"author slug '{slug}'. Add an entry to {AUTHORS_FILE} or "
f"fix the slug."
)
by_author[slug].append((f.src_path, title))

files.append(
File.generated(
config,
"authors/index.md",
content=_render_authors_index(authors_map, by_author),
)
)

config["_authors_map"] = authors_map
return files


def on_page_markdown(markdown, page, config, files):
authors_map = config.get("_authors_map") or {}

if page.file.src_path.startswith("authors/"):
return markdown

fm_authors = (page.meta or {}).get("authors") or []
if not isinstance(fm_authors, list) or not fm_authors:
return markdown

rendered = _render_pattern_bullets(fm_authors, authors_map)

m = CONTRIB_HEADING_RE.search(markdown)
if not m:
sep = "" if markdown.endswith("\n") else "\n"
return (
markdown
+ f"{sep}\n## Contributors & Acknowledgement\n\n{rendered}\n"
)

heading_end = m.end()
next_h = HEADING_RE.search(markdown, heading_end + 1)
section_end = next_h.start() if next_h else len(markdown)
section_body = markdown[heading_end:section_end]

body_lines = section_body.splitlines(keepends=True)
i = 0
while i < len(body_lines) and body_lines[i].strip() == "":
i += 1
while i < len(body_lines):
line = body_lines[i]
if BULLET_LINE_RE.match(line):
i += 1
continue
if line.strip() == "" and i + 1 < len(body_lines) and BULLET_LINE_RE.match(
body_lines[i + 1]
):
i += 1
continue
break
trailing = "".join(body_lines[i:])

rebuilt = markdown[:heading_end] + "\n\n" + rendered + "\n"
if trailing.strip():
if not trailing.startswith("\n"):
rebuilt += "\n"
rebuilt += trailing
rebuilt += markdown[section_end:]
return rebuilt
5 changes: 5 additions & 0 deletions .config/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ nav:
# This adds a link in the navigation bar on the left
- Home: README.md
- Template: PATTERN-TEMPLATE.md
- Authors: authors/index.md

# Associated Git Repo Config
repo_url: https://github.com/CURIOSSorg/curioss-patterns
Expand All @@ -18,6 +19,7 @@ docs_dir: "../"
exclude_docs: |
/node_modules/
/site/
/authors.yml

# Theme Configuration
# Colour options: red, pink, purple, deep purple, indigo, blue, light blue,
Expand Down Expand Up @@ -70,9 +72,12 @@ markdown_extensions:
permalink: ' §'
- footnotes
- sane_lists
- attr_list
plugins:
- search
- tags
hooks:
- hooks/authors.py
extra:
tags:
# Set a tag with a identifier so then an icon can be set on the theme.
Expand Down
34 changes: 33 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,39 @@ Thanks for wanting to contribute! Open a PR or an issue for anything.

## When contributing

- Add your name and ORCID at the bottom of any pattern you edit!
When you contribute to a pattern, add yourself as an author so you get credit
on the website and on the [Authors page](authors/index.md), which lists every
contributor alongside the patterns they've worked on.

There are two short steps:

1. **Add yourself to [`authors.yml`](https://github.com/CURIOSSorg/curioss-patterns/blob/main/authors.yml)** (at the root of this repo)
if you're not already there. Pick a short slug — usually
`firstname-lastname` — and fill in your name, ORCID (optional), and
affiliation (optional). For example:

```yaml
jane-doe:
name: Jane Doe
orcid: 0000-0001-2345-6789
affiliation: Example University
```

2. **Add your slug to the pattern's `authors:` frontmatter** at the top of the
pattern file. For example:

```yaml
---
tags:
- Community Building
authors:
- jane-doe
---
```

You do **not** need to edit the "Contributors & Acknowledgement" section at
the bottom of the pattern — the site generates that automatically from the
frontmatter.

## Local Development

Expand Down
14 changes: 13 additions & 1 deletion PATTERN-TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ tags:
#- Rewards & Recognition
#- Tools & Infrastructure
#- Working with Tech Transfer / External Partners
authors:
# List the slug of every contributor, one per line. Slugs come from `authors.yml`
# at the root of this repo. If you're not in `authors.yml` yet, add yourself
# there first, then list your slug below.
#- your-slug-here
---
# Pattern Name

Expand Down Expand Up @@ -66,4 +71,11 @@ List resources or related patterns for further reading.

## Contributors & Acknowledgement

Recognize individuals or organizations that contributed to this pattern.
The list of contributors is generated automatically from the `authors:` field
in the frontmatter at the top of this file. You don't need to write the names
here yourself — just add your slug to the frontmatter and the site will fill
this section in.

If you want to add extra acknowledgements (for example, a note about AI
assistance, or thanks to people who contributed without an ORCID), add them
as paragraphs below this line and they'll be preserved.
53 changes: 53 additions & 0 deletions authors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Authors of CURIOSS patterns.
#
# Each entry is keyed by a short slug (kebab-case, usually firstname-lastname).
# When you contribute to a pattern, add yourself here (if you're not listed yet),
# then put your slug in that pattern's `authors:` frontmatter list.
#
# Fields:
# name (required) Your full name as you'd like it shown.
# orcid (optional) Your ORCID iD, just the digits like 0000-0001-2345-6789.
# affiliation (optional) Your institution or organisation.

angela-newell:
name: Angela Newell
affiliation: University of Texas at Austin

ciara-flanagan:
name: Ciara Flanagan
orcid: 0009-0005-3153-7673
affiliation: CURIOSS

daniel-shown:
name: Daniel Shown
orcid: 0009-0002-5716-8835
affiliation: Saint Louis University

david-lippert:
name: David Lippert
orcid: 0009-0003-6444-9595
affiliation: George Washington University

emily-lovell:
name: Emily Lovell
orcid: 0000-0001-5531-5956
affiliation: University of California Santa Cruz

jeffrey-young:
name: Jeffrey Young
orcid: 0000-0001-9841-4057
affiliation: OSPO@GT

kendall-fortney:
name: Kendall Fortney
orcid: 0009-0006-3898-0771
affiliation: University of Vermont

laura-langdon:
name: Laura Langdon
affiliation: University of California, Davis

tom-hughes:
name: Tom Hughes
orcid: 0009-0008-7516-3687
affiliation: Carnegie Mellon University
Loading
Loading