diff --git a/.config/hooks/authors.py b/.config/hooks/authors.py new file mode 100644 index 0000000..caea62d --- /dev/null +++ b/.config/hooks/authors.py @@ -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 diff --git a/.config/mkdocs.yml b/.config/mkdocs.yml index 86cea70..e87e7a7 100644 --- a/.config/mkdocs.yml +++ b/.config/mkdocs.yml @@ -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 @@ -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, @@ -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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab988d5..ca56c27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/PATTERN-TEMPLATE.md b/PATTERN-TEMPLATE.md index d879e96..ece63cf 100644 --- a/PATTERN-TEMPLATE.md +++ b/PATTERN-TEMPLATE.md @@ -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 @@ -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. diff --git a/authors.yml b/authors.yml new file mode 100644 index 0000000..062d47d --- /dev/null +++ b/authors.yml @@ -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 diff --git a/embed-wellbeing-into-student-hackathons.md b/embed-wellbeing-into-student-hackathons.md index a82a927..18235c1 100644 --- a/embed-wellbeing-into-student-hackathons.md +++ b/embed-wellbeing-into-student-hackathons.md @@ -3,6 +3,13 @@ tags: - Community Building - Education & Skills - Promoting Best Practices +authors: + - angela-newell + - ciara-flanagan + - david-lippert + - emily-lovell + - laura-langdon + - tom-hughes --- # Embed wellbeing into Student Hackathons @@ -113,10 +120,3 @@ Every team has a mentor - [Lower the barriers to entry for Student Hackathons](lower-the-barriers-to-entry-for-student-hackathons.md) ## Contributors & Acknowledgement - -- Angela Newell, University of Texas at Austin -- Ciara Flanagan, -- David Lippert, George Washington University, -- Emily Lovell, University of California Santa Cruz, -- Laura Langdon, University of California, Davis -- Tom Hughes, Carnegie Mellon University, diff --git a/onboarding-graduate-leads-for-open-source-internship-programs.md b/onboarding-graduate-leads-for-open-source-internship-programs.md index 6fbdf1d..2b6360b 100644 --- a/onboarding-graduate-leads-for-open-source-internship-programs.md +++ b/onboarding-graduate-leads-for-open-source-internship-programs.md @@ -2,12 +2,17 @@ tags: - Education & Skills - Promoting Best Practices +authors: + - ciara-flanagan + - daniel-shown + - kendall-fortney + - jeffrey-young --- # Onboarding Graduate Leads for Open Source Internship Programs ## Pattern Summary -Establish a structured onboarding process for graduate students taking on team lead roles in open source internship programs. +Establish a structured onboarding process for gradluate students taking on team lead roles in open source internship programs. ## Problem / Challenge @@ -133,9 +138,4 @@ A key insight from our experience is the importance of creating spaces where lea ## Contributors & Acknowledgement -- Ciara Flanagan (CURIOSS),() -- Daniel Shown (Saint Louis University), -- Kendall Fortney (University of Vermont), -- Jeffrey Young (OSPO@GT), - A note on AI use: In addition to working from Deep Dive transcripts, capturing learning from our community discussions and other patterns from our members, this pattern was drafted with the help of AI. As a small organization, tools like this help us turn rich conversations into written resources without losing the ideas along the way. As always, there were plenty of human eyes reviewing, editing and improving the content before this pattern made it to publication. Thanks go to our community for the insights. If you do spot any errors, please let us know so we can correct them!