diff --git a/.gitignore b/.gitignore
index bee8a64..d9bf8cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
__pycache__
+.cache
+venv
diff --git a/.woodpecker/readme.yml b/.woodpecker/readme.yml
index 00b1ef7..142953a 100644
--- a/.woodpecker/readme.yml
+++ b/.woodpecker/readme.yml
@@ -1,10 +1,11 @@
when:
- branch: [main]
event: push
+ repo: fediverse/fep
steps:
build:
- image: python:3.11-alpine
+ image: python:3.13-alpine
commands:
- ./scripts/update_issues.py
- ./scripts/create_readme.py
diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml
index 3818563..bde8ac4 100644
--- a/.woodpecker/test.yml
+++ b/.woodpecker/test.yml
@@ -3,8 +3,19 @@ when:
event: [pull_request, push]
steps:
- build:
- image: python:3.11-alpine
+ check_create_readme_works:
+ image: python:3.13-alpine
+ commands:
+ - ./scripts/create_readme.py
+
+ check_new_proposal_works:
+ image: python:3.13-alpine
+ commands:
+ - ./scripts/new_proposal.py Your Title Goes Here
+ - rm -rf fep/7f9d
+
+ run_pytest:
+ image: python:3.13-alpine
commands:
- pip install pytest
- pytest
diff --git a/.woodpecker/website.yml b/.woodpecker/website.yml
new file mode 100644
index 0000000..62822d3
--- /dev/null
+++ b/.woodpecker/website.yml
@@ -0,0 +1,18 @@
+when:
+ - branch: [main]
+ event: push
+
+steps:
+ build:
+ image: python:3.13-alpine
+ commands:
+ - pip install ./scripts
+ - pytest
+ - ./scripts/create_site_pages.py
+ - mkdocs build --config-file scripts/mkdocs.yml
+ deploy_pages:
+ image: codeberg.org/xfix/plugin-codeberg-pages-deploy:1
+ settings:
+ folder: scripts/site
+ ssh_key:
+ from_secret: deploy_key
diff --git a/README.md b/README.md
index 901d2fd..7034c84 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,8 @@ $ echo -n "The title of my proposal" | sha256sum | cut -c-4
b3f0
```
-4. Create a subdirectory of [`fep/`](./fep/) using the identifier you just computed.
-5. Copy the FEP template ([fep-xxxx-template.md](./fep-xxxx-template.md)) to this subdirectory and change the filename appropriately.
+4. Create a subdirectory of [`fep/`](https://codeberg.org/fediverse/fep/src/branch/main/fep/) using the identifier you just computed.
+5. Copy the FEP template ([fep-xxxx-template.md](https://codeberg.org/fediverse/fep/src/branch/main/fep-xxxx-template.md)) to this subdirectory and change the filename appropriately.
6. Use the identifer as the "slug" when filling out the frontmatter.
- For example, if your computed identifier was `abcd`, then your file would be located at `fep/abcd/fep-abcd.md` and your frontmatter would include `slug: "abcd"`.
@@ -33,7 +33,7 @@ b3f0
8. When you are ready to submit the FEP, change the value of `dateReceived` field in the frontmatter to the current date.
9. Create a discussion topic for your FEP. You can use [ActivityPub category](https://socialhub.activitypub.rocks/c/activitypub/5) on the SocialHub forum.
10. Add `discussionsTo` field containing the URL of the discussion topic to the front matter of your FEP.
-11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed.
+11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed.
Alternatively to steps 3. to 6., you can run
@@ -45,7 +45,7 @@ that should create a prefilled template for you.
## Facilitators
-The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues.
+The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](https://codeberg.org/fediverse/fep/src/branch/main/FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues.
## Contributing
@@ -53,8 +53,6 @@ Do you have ideas to improve the FEP Process? Post your suggestions to the issue
## FEPs
-
-
| Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) |
| --- | --- | ----- | ------- | ------ |
| [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md) | `FINAL` | [#201](https://codeberg.org/fediverse/fep/issues/201) | 2020-10-16 | 2021-01-18 |
diff --git a/fep/7d8c/fep-7d8c.md b/fep/7d8c/fep-7d8c.md
index 99a1591..c7a68ea 100644
--- a/fep/7d8c/fep-7d8c.md
+++ b/fep/7d8c/fep-7d8c.md
@@ -39,15 +39,22 @@ Automation for the FEP repository uses the [codeberg woodpecker](https://ci.code
- test runs basic checks on the contained FEPs
- readme updates the `README.md` file and creates the tracking issue
+- create a static website
Automation code is written in python and is contained in the [script folder](https://codeberg.org/fediverse/fep/src/branch/main/scripts).
+### Scripts and tests
+
+FEP uses python scripts to manage some aspects of it. These should be runnable
+without installing packages. In order to ensure these scripts stay working,
+include them in [.woodpecker/test.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/test.yml)
+
## Configuration variables
To be able to do these configuration tasks, you need to be in the __admin__ group. See the
first discussion [here](https://socialhub.activitypub.rocks/t/fep-repo-rights/4797/2).
-The configuration [.woodpecker/readme.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/woodpecker.yml) uses two secrets:
+The configuration [.woodpecker/readme.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/readme.yml) uses two secrets:
- codeberg_api_token, a codeberg API token used to create the tracking issues.
- deploy_key, an SSH private key used to push to the codeberg repository.
@@ -74,7 +81,7 @@ ssh-keygen -t ed25519 -f deploy_key
Do not specify a passphrase. Examples:
-``` bash
+```bash
$ cat deploy_key
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -97,9 +104,56 @@ One can add these secrets on [woodpecker](https://ci.codeberg.org/repos/12388),
The deploy_key needs to be end with a new line when pasting into the woodpecker ui.
+## Development
+
+The tools are located in the `scripts` folder, with the `scripts/fep_tools` folder containing the python package `fep_tools`. Most of this tooling only uses python, e.g.
+`scripts/new_proposal.py` and `scripts/create_readme.md`, so they can be run from any
+environment.
+
+### Running tests
+
+To run the tests, one needs [pytest](https://docs.pytest.org/). We note that the
+tests run from the base folder. The following commands demonstrate how to setup
+a virtual environment, install the dependencies, and run the tests.
+
+```bash
+python -mvenv .venv
+pip install ./scripts
+pytest
+```
+
+One can leave the venv, by running `deactivate`.
+
+
+### Building the website
+
+To build the website, first install the dependencies
+
+```bash
+python -mvenv .venv
+pip install ./scripts
+./scripts/create_site_pages.py
+```
+
+Then run mkdocs via
+
+```bash
+mkdocs serve --config-file scripts/mkdocs.yml
+```
+
+References:
+
+- The website is build with [mkdocs](https://www.mkdocs.org/)
+- and uses [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) for its many plugins
+
+### Automatic deployment
+
+See [website.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/website.yml).
+
## Changes
- Added discussion link to tracking issue, use table to format (@helge, 2025-03-07)
+- Added static website stuff [fep#673](https://codeberg.org/fediverse/fep/pulls/673)
## References
diff --git a/scripts/.gitignore b/scripts/.gitignore
index 665f454..fa23a83 100644
--- a/scripts/.gitignore
+++ b/scripts/.gitignore
@@ -1 +1,10 @@
config*.json
+site
+docs/fep
+docs/withdrawn.md
+docs/final.md
+docs/draft.md
+docs/index.md
+build
+fep.egg-info
+uv.lock
diff --git a/scripts/create_issue.py b/scripts/create_issue.py
index ca80976..6961383 100755
--- a/scripts/create_issue.py
+++ b/scripts/create_issue.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
from argparse import ArgumentParser
-from tools.issue import create_issue
+from fep_tools.issue import create_issue
import json
@@ -12,5 +12,4 @@ args = parser.parse_args()
with open("scripts/config.json") as f:
config = json.load(f)
-create_issue(config['owner'], config['repo'], config['token'], args.fep)
-
+create_issue(config["owner"], config["repo"], config["token"], args.fep)
diff --git a/scripts/create_readme.py b/scripts/create_readme.py
index 0269bd7..08fd0ba 100755
--- a/scripts/create_readme.py
+++ b/scripts/create_readme.py
@@ -2,17 +2,11 @@
import json
-from tools import index, Readme
+from fep_tools import data_for_json_file, Readme
with open("README.md", "w") as f1:
f1.writelines(Readme().content)
-result = []
-for fep in index():
- data = fep.parsed_frontmatter
- data["title"] = fep.title
- data["implementations"] = fep.implementations
- result.append(data)
with open("index.json", "w") as index_file:
- json.dump(result, index_file, indent=2)
+ json.dump(data_for_json_file(), index_file, indent=2)
diff --git a/scripts/create_site_pages.py b/scripts/create_site_pages.py
new file mode 100755
index 0000000..b1ea338
--- /dev/null
+++ b/scripts/create_site_pages.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+from fep_tools import index
+from fep_tools.site_fep_page import make_page_for_fep
+from fep_tools.site_overview_page import write_overview_file
+
+from fep_tools import Readme
+
+path_prefix = "scripts/docs"
+
+with open(f"{path_prefix}/index.md", "w") as fp:
+ fp.write("""---
+hide:
+- navigation
+---
+
+# Fediverse Enhancement Proposals
+
+
+
+- [:material-file-document: Final Proposals](./final.md)
+- [:material-file-document-edit: Draft Proposals](./draft.md)
+
+
+
+""")
+
+ fp.writelines(Readme(include_fep_table=False).content)
+
+
+for file, status in [
+ ("index.html", "FINAL"),
+ ("draft.html", "DRAFT"),
+ ("withdrawn.html", "WITHDRAWN"),
+]:
+ filename = f"{path_prefix}/{status.lower()}.md"
+
+ write_overview_file(filename, status)
+
+
+for fep in index():
+ make_page_for_fep(fep)
diff --git a/scripts/docs/assets/codeberg.png b/scripts/docs/assets/codeberg.png
new file mode 100644
index 0000000..0d5d02e
Binary files /dev/null and b/scripts/docs/assets/codeberg.png differ
diff --git a/scripts/docs/assets/logo.png b/scripts/docs/assets/logo.png
new file mode 100644
index 0000000..6d958d0
Binary files /dev/null and b/scripts/docs/assets/logo.png differ
diff --git a/scripts/docs/javascripts/tablesort.js b/scripts/docs/javascripts/tablesort.js
new file mode 100644
index 0000000..4014300
--- /dev/null
+++ b/scripts/docs/javascripts/tablesort.js
@@ -0,0 +1,6 @@
+document$.subscribe(function () {
+ var tables = document.querySelectorAll("article table:not([class])");
+ tables.forEach(function (table) {
+ new Tablesort(table);
+ });
+});
diff --git a/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js b/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js
new file mode 100644
index 0000000..65a83b1
--- /dev/null
+++ b/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js
@@ -0,0 +1,6 @@
+/*!
+ * tablesort v5.2.1 (2021-10-30)
+ * http://tristen.ca/tablesort/demo/
+ * Copyright (c) 2021 ; Licensed MIT
+*/
+!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a,b){return a.getAttribute(b.sortAttribute||"data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&n.push(m),o++;if(!n)return}for(o=0;o list[dict[str, str]]:
+ result = []
+ for fep in index():
+ data = fep.parsed_frontmatter
+ data["title"] = fep.title
+ data["implementations"] = fep.implementation_count
+ result.append(data)
+
+ return result
diff --git a/scripts/tools/fep_file.py b/scripts/fep_tools/fep_file.py
similarity index 54%
rename from scripts/tools/fep_file.py
rename to scripts/fep_tools/fep_file.py
index 6e50c4b..37b7225 100644
--- a/scripts/tools/fep_file.py
+++ b/scripts/fep_tools/fep_file.py
@@ -1,22 +1,38 @@
-def unquote(value):
+from dataclasses import dataclass, field
+
+
+def unquote(value: str) -> str:
if value.startswith('"') and value.endswith('"'):
return value[1:-1]
else:
return value
+@dataclass
class FepFile:
- def __init__(self, fep):
- self.fep = fep
+ fep: str
+ frontmatter: list[str] = field(default_factory=list)
+ content: list[str] = field(default_factory=list)
+
+ def __post_init__(self):
with open(self.filename) as f:
- self.frontmatter, self.content = FepFile.parsefile(f)
+ lines = f.readlines()
+
+ status = 0
+
+ for line in lines:
+ if line == "---\n" and status <= 2:
+ status += 1
+ elif status == 1:
+ self.frontmatter.append(line.removesuffix("\n"))
+ elif status >= 2:
+ self.content.append(line.removesuffix("\n"))
@property
- def filename(self):
+ def filename(self) -> str:
return f"fep/{self.fep}/fep-{self.fep}.md"
- @property
- def summary(self):
+ def find_section_by_name(self, name):
result = []
is_summary = False
for x in self.content:
@@ -24,9 +40,13 @@ class FepFile:
if x.startswith("##"):
return "\n".join(result)
result.append(x)
- elif x == "## Summary":
+ elif x == f"## {name}":
is_summary = True
+ @property
+ def summary(self):
+ return self.find_section_by_name("Summary")
+
def write(self):
with open(self.filename, "w") as f:
f.write("---\n")
@@ -42,7 +62,11 @@ class FepFile:
return {a: unquote(b.strip()) for a, b in split}
@property
- def title(self):
+ def status(self):
+ return self.parsed_frontmatter["status"]
+
+ @property
+ def title(self) -> str:
titles = [x for x in self.content if x.startswith("# ")]
assert len(titles) > 0
@@ -56,25 +80,23 @@ class FepFile:
return true_title
@property
- def implementations(self):
+ def implementation_count(self):
if self.parsed_frontmatter.get("type") != "implementation":
return 0
implementations = []
in_section = False
for line in self.content:
- if line.startswith('#') and "Implementations" in line:
+ if line.startswith("#") and "Implementations" in line:
in_section = True
- elif in_section is True and line.startswith('#'):
+ elif in_section is True and line.startswith("#"):
in_section = False
- elif (
- in_section is True
- and (line.startswith("-") or line.startswith("*"))
- ):
+ elif in_section is True and (line.startswith("-") or line.startswith("*")):
implementations.append(line)
+
return len(implementations)
@staticmethod
- def parsefile(f):
+ def parsefile(f) -> tuple[list[str], list[str]]:
lines = f.readlines()
status = 0
@@ -90,3 +112,21 @@ class FepFile:
content.append(line.removesuffix("\n"))
return frontmatter, content
+
+ def frontmatter_table(self) -> str:
+ keys = " | ".join(self.parsed_frontmatter.keys())
+ values = " | ".join(self.parsed_frontmatter.values())
+ divider = " | ".join(["---"] * len(self.parsed_frontmatter))
+
+ return f"""
+| {keys} |
+| {divider} |
+| {values} |
+"""
+
+ def content_and_title(self) -> tuple[str, str]:
+ for j, line in enumerate(self.content):
+ if line.startswith("# "):
+ return line, "\n".join(self.content[j + 1 :])
+
+ raise Exception("Could not determine title and content")
diff --git a/scripts/tools/issue.py b/scripts/fep_tools/issue.py
similarity index 97%
rename from scripts/tools/issue.py
rename to scripts/fep_tools/issue.py
index 3c221eb..33dfc43 100644
--- a/scripts/tools/issue.py
+++ b/scripts/fep_tools/issue.py
@@ -72,7 +72,7 @@ def update_fep_file_with_date_received(fep_file: FepFile, date_received: date):
fep_file.frontmatter.append(f"dateReceived: {date_received.isoformat()}")
-def create_issue(owner, repo, token, slug):
+def create_issue(owner: str, repo: str, token: str, slug: str):
fep_file = FepFile(slug)
if "trackingIssue" in fep_file.parsed_frontmatter:
diff --git a/scripts/fep_tools/site_fep_page.py b/scripts/fep_tools/site_fep_page.py
new file mode 100644
index 0000000..87ac9ab
--- /dev/null
+++ b/scripts/fep_tools/site_fep_page.py
@@ -0,0 +1,124 @@
+import re
+import shutil
+from pathlib import Path
+
+try:
+ import nh3
+ from markdown import markdown
+except Exception:
+ ...
+
+from .fep_file import FepFile
+from .table import TableLineBuilder
+
+
+def transform_key(x: str) -> str:
+ if x in ["dateReceived", "dateWithdrawn", "trackingIssue"]:
+ return re.sub(r"(? str:
+ """Creates a markdown table containing FEP metadata"""
+ matter_keys = [transform_key(x) for x in fep.parsed_frontmatter.keys()]
+ matter_keys = [x for x in possible_matter_keys if x in matter_keys] + ["repository"]
+
+ line_builder = TableLineBuilder(fep=fep, columns=list(matter_keys))
+ keys = " | ".join(x.replace("_", " ").capitalize() for x in matter_keys)
+ values = line_builder.line
+
+ divider = " | ".join(["---"] * len(matter_keys))
+
+ return f"""
+| {keys} |
+| {divider} |
+{values}
+"""
+
+
+def clean_markdown(text: str) -> str:
+ """Transforms markdown to a plain text string. To be used in the frontmatter"""
+ transformed = nh3.clean(markdown(text), tags=set())
+ transformed = re.sub(r"\[(.+?)\]\[(.+?)\]", r"\1", transformed)
+ transformed = transformed.replace(
+ ":", " "
+ ) # See https://codeberg.org/fediverse/fep/pulls/673/files#issuecomment-7514311
+ return transformed.replace("\n", " ")
+
+
+def frontmatter_for_fep(fep: FepFile) -> str:
+ summary = fep.summary
+ if summary is None:
+ return """---
+hide:
+ - navigation
+---
+
+"""
+ description = clean_markdown(summary)
+
+ return f"""---
+hide:
+ - navigation
+description: {description}
+---
+
+"""
+
+
+def transform_content(content: str) -> str:
+ """Transforms links to other FEPs. These need to be adjusted due to
+ the renaming of `fep-slug.md` to `index.md`."""
+ content = re.sub(
+ r"\[(.+?)\]\((\.\./.+)/fep-(.+).md\)", r"[\1](\2/index.md)", content
+ )
+ content = re.sub(r"\[(.+?)\]: (\.\./.+)/fep-(.+).md", r"[\1]: \2/index.md", content)
+ return content
+
+
+def fep_to_site_mkdocs(fep: FepFile) -> str:
+ """Transforms the FEP to markdown suitable for publishing as a static website"""
+ title, content = fep.content_and_title()
+
+ file_content = (
+ frontmatter_for_fep(fep)
+ + title
+ + "\n\n"
+ + format_metatable(fep)
+ + "\n\n"
+ + transform_content(content)
+ )
+
+ return file_content
+
+
+def make_page_for_fep(fep: FepFile):
+ """Copies relevant files for the FEP and adjusts index.md"""
+ slug = fep.fep
+
+ base_path = f"scripts/docs/fep/{slug}/"
+ Path(base_path).mkdir(exist_ok=True, parents=True)
+
+ shutil.rmtree(base_path)
+ shutil.copytree(f"fep/{slug}/", base_path)
+ shutil.move(f"{base_path}/fep-{slug}.md", f"{base_path}/index.md")
+
+ with open(f"{base_path}/index.md", "w") as fp:
+ fp.write(fep_to_site_mkdocs(fep))
diff --git a/scripts/fep_tools/site_overview_page.py b/scripts/fep_tools/site_overview_page.py
new file mode 100644
index 0000000..d5d9222
--- /dev/null
+++ b/scripts/fep_tools/site_overview_page.py
@@ -0,0 +1,39 @@
+from . import index, table_for_feps
+
+markdown_frontmatter = """---
+hide:
+- toc
+- navigation
+---
+"""
+
+
+def columns_for_status(status: str) -> list[str]:
+ columns = [
+ "title",
+ "repo_link_image",
+ "tracking_issue",
+ "discussions",
+ "date_received",
+ ]
+ if status == "FINAL":
+ columns.append("date_finalized")
+ elif status == "WITHDRAWN":
+ columns.append("date_withdrawn")
+ return columns
+
+
+def write_overview_file(filename: str, status: str) -> None:
+ columns = columns_for_status(status)
+ feps = [x for x in index() if x.status == status]
+
+ table = "".join(
+ table_for_feps(feps, columns=columns, generate_title=True, format="static")
+ )
+
+ title = status[0] + status[1:].lower()
+
+ with open(filename, "w") as f:
+ f.write(markdown_frontmatter)
+ f.write(f"\n# {title}\n")
+ f.write(table)
diff --git a/scripts/fep_tools/table.py b/scripts/fep_tools/table.py
new file mode 100644
index 0000000..6ace63a
--- /dev/null
+++ b/scripts/fep_tools/table.py
@@ -0,0 +1,138 @@
+from dataclasses import dataclass
+
+from .fep_file import FepFile
+
+
+repo_base = "https://codeberg.org/fediverse/fep/src/branch/main/"
+
+
+@dataclass
+class TableLineBuilder:
+ fep: FepFile
+ columns: list[str]
+ format: str = "readme"
+
+ @property
+ def parsed(self):
+ return self.fep.parsed_frontmatter
+
+ @property
+ def title(self):
+ if self.format == "readme":
+ return f"[FEP-{self.fep.fep}: {self.fep.title}](./{self.fep.filename})"
+
+ return f"[FEP-{self.fep.fep}: {self.fep.title}](fep/{self.fep.fep}/index.md)"
+
+ @property
+ def repo_link_image(self):
+ return f"[
]({repo_base}{self.fep.filename})"
+
+ @property
+ def tracking_issue(self):
+ url = self.parsed["trackingIssue"]
+ url_number = url.split("/")[-1]
+ return f"[#{url_number}]({url})"
+
+ @property
+ def status(self):
+ status = self.parsed["status"]
+ return f"`{status}`"
+
+ @property
+ def date_received(self):
+ return self.parsed.get("dateReceived", "-")
+
+ @property
+ def date_finalized(self):
+ return self.parsed.get("dateFinalized", "-")
+
+ @property
+ def date_withdrawn(self):
+ return self.parsed.get("dateWithdrawn", "-")
+
+ @property
+ def date_final(self):
+ if "dateFinalized" in self.parsed:
+ return self.parsed["dateFinalized"]
+ if "dateWithdrawn" in self.parsed:
+ return self.parsed["dateWithdrawn"]
+ return "-"
+
+ @property
+ def discussions(self):
+ discussions = self.parsed["discussionsTo"]
+
+ if discussions == self.parsed["trackingIssue"]:
+ return "-"
+
+ return f"[Discussions]({discussions})"
+
+ @property
+ def repository(self) -> str:
+ url = f"{repo_base}fep/{self.fep.fep}/fep-{self.fep.fep}.md"
+ return f"[codeberg]({url})"
+
+ def get_attribute(self, column: str):
+ try:
+ return self.__getattribute__(column)
+ except AttributeError:
+ return self.parsed.get(column, "-")
+
+ @property
+ def line(self) -> str:
+ values = [self.get_attribute(column) for column in self.columns]
+ middle = " | ".join(values)
+ return f"| {middle} |\n"
+
+
+@dataclass
+class TableTitle:
+ columns: list[str]
+
+ column_to_title = {
+ "title": "Title",
+ "status": "Status",
+ "tracking_issue": "Tracking issue",
+ "discussions": "Discussions",
+ "date_received": "Received",
+ "date_final": "Finalized / Withdrawn",
+ "date_finalized": "Finalized",
+ "date_withdrawn": "Withdrawn",
+ }
+
+ @property
+ def title_line(self):
+ center = " | ".join(
+ [self.column_to_title.get(column, "") for column in self.columns]
+ )
+
+ return f"| {center} |\n"
+
+ @property
+ def second_line(self):
+ center = " | ".join(["---"] * len(self.columns))
+ return f"| {center} |\n"
+
+ @property
+ def result(self) -> list[str]:
+ return [self.title_line, self.second_line]
+
+
+def table_for_feps(
+ feps: list[FepFile],
+ columns: list[str] = [
+ "title",
+ "status",
+ "tracking_issue",
+ "date_received",
+ # Finalized / Withdrawn
+ "date_final",
+ ],
+ generate_title: bool = False,
+ format: str = "readme",
+):
+ lines = [TableLineBuilder(fep, columns, format=format).line for fep in feps]
+ if generate_title:
+ return TableTitle(columns).result + lines
+
+ return lines
diff --git a/scripts/fep_tools/test_fep_file.py b/scripts/fep_tools/test_fep_file.py
new file mode 100644
index 0000000..7214fd7
--- /dev/null
+++ b/scripts/fep_tools/test_fep_file.py
@@ -0,0 +1,13 @@
+from . import fep_file
+
+
+def test_fep_file():
+ file = fep_file.FepFile("a4ed")
+
+ assert file.status == "FINAL"
+
+
+def test_implementation_count():
+ file = fep_file.FepFile("8b32")
+
+ assert file.implementation_count > 2
diff --git a/scripts/tools/test_feps.py b/scripts/fep_tools/test_feps.py
similarity index 88%
rename from scripts/tools/test_feps.py
rename to scripts/fep_tools/test_feps.py
index 5fb15be..4e447ed 100644
--- a/scripts/tools/test_feps.py
+++ b/scripts/fep_tools/test_feps.py
@@ -3,7 +3,7 @@ import pytest
from urllib.parse import urlparse
-from scripts.tools import get_fep_ids, FepFile, title_to_slug
+from scripts.fep_tools import get_fep_ids, FepFile, title_to_slug
@pytest.mark.parametrize("fep", get_fep_ids())
@@ -20,9 +20,9 @@ def test_fep_front_matter(fep):
discussions_to = parsed_frontmatter["discussionsTo"]
- assert not urlparse(discussions_to).netloc.endswith(
- ".example"
- ), "Update discussionsTo to a valid URL for a discussion topic"
+ assert not urlparse(discussions_to).netloc.endswith(".example"), (
+ "Update discussionsTo to a valid URL for a discussion topic"
+ )
if parsed_frontmatter["status"] == "FINAL":
assert "dateFinalized" in parsed_frontmatter
@@ -36,6 +36,7 @@ def test_fep_front_matter(fep):
if "type" in parsed_frontmatter:
assert parsed_frontmatter["type"] in ["informational", "implementation"]
+
@pytest.mark.parametrize("fep", get_fep_ids())
def test_fep_content(fep):
fep_file = FepFile(fep)
diff --git a/scripts/tools/test_issue.py b/scripts/fep_tools/test_issue.py
similarity index 100%
rename from scripts/tools/test_issue.py
rename to scripts/fep_tools/test_issue.py
diff --git a/scripts/fep_tools/test_json.py b/scripts/fep_tools/test_json.py
new file mode 100644
index 0000000..b564dfc
--- /dev/null
+++ b/scripts/fep_tools/test_json.py
@@ -0,0 +1,12 @@
+import json
+from . import data_for_json_file
+
+
+def test_data_for_json_file():
+ result = data_for_json_file()
+
+ assert isinstance(result, list)
+
+ dumped = json.dumps(result)
+
+ assert isinstance(dumped, str)
diff --git a/scripts/tools/test_readme.py b/scripts/fep_tools/test_readme.py
similarity index 62%
rename from scripts/tools/test_readme.py
rename to scripts/fep_tools/test_readme.py
index 0c79359..7bb511c 100644
--- a/scripts/tools/test_readme.py
+++ b/scripts/fep_tools/test_readme.py
@@ -1,6 +1,6 @@
import pytest
-from scripts.tools import Readme
+from scripts.fep_tools import Readme
@pytest.mark.skip("Only is correct for main branch and not pull requests")
@@ -11,3 +11,9 @@ def test_readme():
expected = Readme().content
assert lines == expected
+
+
+def test_readme_can_be_created():
+ content = Readme().content
+
+ assert isinstance(content, list)
diff --git a/scripts/fep_tools/test_site_fep_page.py b/scripts/fep_tools/test_site_fep_page.py
new file mode 100644
index 0000000..ed6bcf1
--- /dev/null
+++ b/scripts/fep_tools/test_site_fep_page.py
@@ -0,0 +1,71 @@
+import pytest
+from . import fep_file
+from .site_fep_page import format_metatable, clean_markdown, transform_content
+
+
+def skip_due_to_no_nh3_markdown():
+ try:
+ import nh3 # type: ignore # noqa
+ import markdown # type: ignore # noqa
+ except Exception:
+ return True
+ return False
+
+
+def test_format_metatable():
+ file = fep_file.FepFile("a4ed")
+
+ table = format_metatable(file)
+
+ assert (
+ table
+ == """
+| Authors | Status | Date received | Date final | Tracking issue | Discussions | Repository |
+| --- | --- | --- | --- | --- | --- | --- |
+| pukkamustard | `FINAL` | 2020-10-16 | 2021-01-18 | [#201](https://codeberg.org/fediverse/fep/issues/201) | - | [codeberg](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md) |
+
+"""
+ )
+
+
+@pytest.mark.skipif(
+ skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed"
+)
+@pytest.mark.parametrize(
+ "text,expected",
+ [
+ ("[link](http://test.example)", "link"),
+ ("[link][link]", "link"),
+ ("[a][a] [b][b]", "a b"),
+ ("a:b", "a b"),
+ ],
+)
+def test_clean_markdown(text: str, expected: str):
+ result = clean_markdown(text)
+ assert result == expected
+
+
+@pytest.mark.skipif(
+ skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed"
+)
+@pytest.mark.parametrize(
+ "unchanged",
+ [
+ "[link](http://remote.tests)",
+ "[fep-f1d5](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)",
+ "[fep-f1d5]: https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md",
+ ],
+)
+def test_content_unchanged(unchanged: str):
+ assert transform_content(unchanged) == unchanged
+
+
+@pytest.mark.skipif(
+ skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed"
+)
+def test_content():
+ changed = "[link](../4adb/fep-4adb.md)"
+ assert transform_content(changed) == "[link](../4adb/index.md)"
+
+ changed = "[FEP-4adb]: ../4adb/fep-4adb.md"
+ assert transform_content(changed) == "[FEP-4adb]: ../4adb/index.md"
diff --git a/scripts/fep_tools/test_table.py b/scripts/fep_tools/test_table.py
new file mode 100644
index 0000000..c0e2377
--- /dev/null
+++ b/scripts/fep_tools/test_table.py
@@ -0,0 +1,33 @@
+from .fep_file import FepFile
+
+from .table import table_for_feps, TableTitle
+
+
+def test_table_for_feps():
+ fep = FepFile("a4ed")
+ result = table_for_feps([fep])
+
+ assert len(result) == 1
+ line = result[0]
+
+ assert (
+ line
+ == "| [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md) | `FINAL` | [#201](https://codeberg.org/fediverse/fep/issues/201) | 2020-10-16 | 2021-01-18 |\n"
+ )
+
+
+def test_table_title():
+ columns = [
+ "title",
+ "tracking_issue",
+ "discussions",
+ "date_received",
+ "date_final",
+ ]
+
+ result = TableTitle(columns).result
+
+ assert result == [
+ "| Title | Tracking issue | Discussions | Received | Finalized / Withdrawn |\n",
+ "| --- | --- | --- | --- | --- |\n",
+ ]
diff --git a/scripts/impl_count.py b/scripts/impl_count.py
index 8788274..a395482 100644
--- a/scripts/impl_count.py
+++ b/scripts/impl_count.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
-from tools import index
+from fep_tools import index
result = []
print("| Title | Status | Count |")
@@ -9,6 +9,8 @@ for fep in index():
slug = fep.parsed_frontmatter["slug"]
status = fep.parsed_frontmatter["status"]
typ = fep.parsed_frontmatter.get("type") or "-"
- if fep.implementations == 0:
+ if fep.implementation_count == 0:
continue
- print(f"| [{fep.title}](https://codeberg.org/fediverse/fep/src/branch/main/fep/{slug}/fep-{slug}.md) | {status} | {fep.implementations} |")
+ print(
+ f"| [{fep.title}](https://codeberg.org/fediverse/fep/src/branch/main/fep/{slug}/fep-{slug}.md) | {status} | {fep.implementation_count} |"
+ )
diff --git a/scripts/mkdocs.yml b/scripts/mkdocs.yml
new file mode 100644
index 0000000..a4dc088
--- /dev/null
+++ b/scripts/mkdocs.yml
@@ -0,0 +1,56 @@
+site_name: Fediverse Enhancement Proposals
+site_url: https://helge.codeberg.page/fep
+repo_url: https://codeberg.org/fediverse/fep
+repo_name: fediverse/fep
+
+theme:
+ name: material
+ logo: assets/logo.png
+ favicon: assets/logo.png
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+ features:
+ - navigation.tabs
+ font: false
+
+markdown_extensions:
+ - toc:
+ permalink: true
+ - github-callouts
+ - tables
+ - attr_list
+ - md_in_html
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - admonition
+ - pymdownx.details
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - footnotes
+
+extra_javascript:
+ - javascripts/tablesort@5.3.0/tablesort.min.js
+ - javascripts/tablesort.js
+extra_css:
+ - styles/theme.css
+nav:
+ - index.md
+ - final.md
+ - draft.md
+ - withdrawn.md
+
+plugins:
+ - search
diff --git a/scripts/new_proposal.py b/scripts/new_proposal.py
index 143f87e..5c8e161 100755
--- a/scripts/new_proposal.py
+++ b/scripts/new_proposal.py
@@ -4,7 +4,7 @@ from argparse import ArgumentParser
import os
import shutil
-from tools import title_to_slug, FepFile
+from fep_tools import title_to_slug, FepFile
def build_parser():
diff --git a/scripts/pull_request.py b/scripts/pull_request.py
index d6da146..fa01775 100644
--- a/scripts/pull_request.py
+++ b/scripts/pull_request.py
@@ -1,6 +1,6 @@
from argparse import ArgumentParser
from datetime import date, timedelta
-from tools import FepFile
+from fep_tools import FepFile
import json
diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml
new file mode 100644
index 0000000..f6eff7d
--- /dev/null
+++ b/scripts/pyproject.toml
@@ -0,0 +1,19 @@
+[project]
+name = "fep"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = [
+ "mkdocs>=1.6.1",
+ "mkdocs-material[imaging]>=9.5.40",
+ "pytest>=8.3.4",
+ "markdown>=3.7",
+ "pytest-watcher>=0.4.3",
+ "markdown-callouts>=0.4.0",
+ "fep",
+ "nh3>=0.3.0",
+]
+
+[tool.setuptools]
+packages = ["fep_tools"]
diff --git a/scripts/backmatter.md b/scripts/snippets/backmatter.md
similarity index 100%
rename from scripts/backmatter.md
rename to scripts/snippets/backmatter.md
diff --git a/scripts/snippets/fep_title.md b/scripts/snippets/fep_title.md
new file mode 100644
index 0000000..b863435
--- /dev/null
+++ b/scripts/snippets/fep_title.md
@@ -0,0 +1,4 @@
+## FEPs
+
+| Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) |
+| --- | --- | ----- | ------- | ------ |
diff --git a/scripts/frontmatter.md b/scripts/snippets/frontmatter.md
similarity index 67%
rename from scripts/frontmatter.md
rename to scripts/snippets/frontmatter.md
index 9a8a5ca..96d29d2 100644
--- a/scripts/frontmatter.md
+++ b/scripts/snippets/frontmatter.md
@@ -1,9 +1,3 @@
-# Fediverse Enhancement Proposals
-
-
-
-This is the Git repository containing Fediverse Enhancment Proposals (FEPs).
-
A Fediverse Enhancement Proposal (FEP) is a document that provides information to the Fediverse community. The goal of a FEP is to improve interoperability and well-being of diverse services, applications and communities that form the Fediverse.
The FEP Process is an initiative of the [SocialHub](https://socialhub.activitypub.rocks) developer community, a liaison of the [W3C Social Web Incubator Community Group](https://www.w3.org/community/SocialCG/). For ongoing and past discussion see the [SocialHub FEP category](https://socialhub.activitypub.rocks/c/standards/fep/54).
@@ -23,8 +17,8 @@ $ echo -n "The title of my proposal" | sha256sum | cut -c-4
b3f0
```
-4. Create a subdirectory of [`fep/`](./fep/) using the identifier you just computed.
-5. Copy the FEP template ([fep-xxxx-template.md](./fep-xxxx-template.md)) to this subdirectory and change the filename appropriately.
+4. Create a subdirectory of [`fep/`](https://codeberg.org/fediverse/fep/src/branch/main/fep/) using the identifier you just computed.
+5. Copy the FEP template ([fep-xxxx-template.md](https://codeberg.org/fediverse/fep/src/branch/main/fep-xxxx-template.md)) to this subdirectory and change the filename appropriately.
6. Use the identifer as the "slug" when filling out the frontmatter.
- For example, if your computed identifier was `abcd`, then your file would be located at `fep/abcd/fep-abcd.md` and your frontmatter would include `slug: "abcd"`.
@@ -33,7 +27,7 @@ b3f0
8. When you are ready to submit the FEP, change the value of `dateReceived` field in the frontmatter to the current date.
9. Create a discussion topic for your FEP. You can use [ActivityPub category](https://socialhub.activitypub.rocks/c/activitypub/5) on the SocialHub forum.
10. Add `discussionsTo` field containing the URL of the discussion topic to the front matter of your FEP.
-11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed.
+11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed.
Alternatively to steps 3. to 6., you can run
@@ -45,15 +39,9 @@ that should create a prefilled template for you.
## Facilitators
-The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues.
+The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](https://codeberg.org/fediverse/fep/src/branch/main/FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues.
## Contributing
Do you have ideas to improve the FEP Process? Post your suggestions to the issue tracker, or on the [SocialHub](https://socialhub.activitypub.rocks) forum. The SocialHub developer community is a "DoOcracy" which means: “pick up any task you want, and then steer it to completion”. Your contributions are most welcome, so delve in and find out how you can help.
-## FEPs
-
-
-
-| Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) |
-| --- | --- | ----- | ------- | ------ |
diff --git a/scripts/snippets/frontmatter_git.md b/scripts/snippets/frontmatter_git.md
new file mode 100644
index 0000000..454aabc
--- /dev/null
+++ b/scripts/snippets/frontmatter_git.md
@@ -0,0 +1,6 @@
+# Fediverse Enhancement Proposals
+
+
+
+This is the Git repository containing Fediverse Enhancment Proposals (FEPs).
+
diff --git a/scripts/tools/__init__.py b/scripts/tools/__init__.py
deleted file mode 100644
index e772147..0000000
--- a/scripts/tools/__init__.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import glob
-import hashlib
-
-from .fep_file import FepFile
-
-
-def title_to_slug(title):
- return hashlib.sha256(title.encode("utf-8")).hexdigest()[:4]
-
-
-def get_fep_ids():
- for fep in glob.glob("fep/*"):
- yield fep.removeprefix("fep/")
-
-
-def build_url_link(url):
- url_number = url.split("/")[-1]
- return f"[#{url_number}]({url})"
-
-
-def index():
- fep_files = [FepFile(fep) for fep in get_fep_ids()]
- fep_files = sorted(
- fep_files,
- key=lambda x: (x.parsed_frontmatter["dateReceived"], x.parsed_frontmatter["slug"]),
- )
- return fep_files
-
-
-class Readme:
- @property
- def content(self):
- return self.frontmatter + self.table + self.backmatter
-
- @property
- def frontmatter(self):
- with open("scripts/frontmatter.md") as f:
- return f.readlines()
-
- @property
- def backmatter(self):
- with open("scripts/backmatter.md") as f:
- return f.readlines()
-
- @property
- def table(self):
- result = []
-
- for fep in index():
- link = f"[FEP-{fep.fep}: {fep.title}](./{fep.filename})"
- parsed = fep.parsed_frontmatter
-
- tracking_issue = build_url_link(parsed["trackingIssue"])
-
- if "dateFinalized" in parsed:
- date_final = parsed["dateFinalized"]
- elif "dateWithdrawn" in parsed:
- date_final = parsed["dateWithdrawn"]
- else:
- date_final = "-"
- result.append(
- f"""| {link} | `{parsed["status"]}` | {tracking_issue} | {parsed["dateReceived"]} | {date_final} |\n"""
- )
- return result
diff --git a/scripts/update_issues.py b/scripts/update_issues.py
index a79013c..3f11d88 100755
--- a/scripts/update_issues.py
+++ b/scripts/update_issues.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
-from tools import get_fep_ids, FepFile
-from tools.issue import create_issue
+from fep_tools import get_fep_ids, FepFile
+from fep_tools.issue import create_issue
import os
@@ -9,10 +9,23 @@ owner = os.environ.get("CI_REPO_OWNER")
repo = os.environ.get("CI_REPO_NAME")
token = os.environ.get("CODEBERG_API_TOKEN")
+if not isinstance(owner, str):
+ print("Please set the owner of the repostory at CI_REPO_OWNER")
+ exit(1)
+
+if not isinstance(repo, str):
+ print("Please set the repository at CI_REPO_NAME")
+ exit(1)
+
+if not isinstance(token, str):
+ print("Please provide a codeberg api token")
+ exit(1)
+
+
for slug in get_fep_ids():
fep_file = FepFile(slug)
tracking_issue = fep_file.parsed_frontmatter.get("trackingIssue")
if not tracking_issue:
print(slug, tracking_issue)
- create_issue(owner, repo, token, slug)
\ No newline at end of file
+ create_issue(owner, repo, token, slug)