diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce191e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pyd +__pycache__ +.DS_Store +output/ \ No newline at end of file diff --git a/makesite.py b/makesite.py new file mode 100644 index 0000000..68f91b6 --- /dev/null +++ b/makesite.py @@ -0,0 +1,388 @@ +""" +Script to build a website from a bunch of markdown files. +Inspired by https://github.com/sunainapai/makesite +Tweaked for almarklein.org +Then for pygfx.org +""" + +import os +import shutil +import webbrowser + +import markdown +import pygments +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name + + +TITLE = "pygfx.org" + +NAV = { + "Main": "index", + "Sponsor": "sponsor", + # "Blog": "blog", + # "Archive": "archive", + # "Social": { + # 'Twitter': 'https://twitter.com/pygfx', + # }, +} + +NEWS = { + "Released pygfx v0.5.0": "https://github.com/pygfx/pygfx/releases/tag/v0.5.0", + "Released wgpu-py v0.18.1": "https://github.com/pygfx/wgpu-py/releases/tag/v0.18.1", +} + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +OUT_DIR = os.path.join(THIS_DIR, "output") +STATIC_DIR = os.path.join(THIS_DIR, "static") +PAGES_DIR = os.path.join(THIS_DIR, "pages") +POSTS_DIR = os.path.join(THIS_DIR, "posts") + + +REDIRECT = '' + + +def create_menu(page): + """ Create the menu for the given page. + """ + menu = [""] + + menu.append('Pages') + for title, target in NAV.items(): + if isinstance(target, str): + if target.startswith(("https://", "http://", "/")): + menu.append(f"{title}") + else: + menu.append(f"{title}") + if target == page.name: + menu[-1] = menu[-1].replace("{title}") + if target.get("", None) == page.name: + menu[-1] = menu[-1].replace("{subtitle}") + else: + menu.append( + f"{subtitle}" + ) + if subtarget == page.name: + menu[-1] = menu[-1].replace("class='", "class='current ") + else: + raise RuntimeError(f"Unexpected NAV entry {type(target)}") + + subtitles = [title for level, title in page.headers if level == 2] + if subtitles: + menu.append("
Current page") + menu += [ + f"{title}" for title in subtitles + ] + + if NEWS: + menu.append('
News') + for title, url in NEWS.items(): + # menu.append(f"{title}") + menu.append(f"{title}") + + return "
".join(menu) + + +def create_blog_relatated_pages(posts): + """ Create blog overview page. + """ + + # Filter and sort + posts = [ + post for post in posts.values() if post.date and not post.name.startswith("_") + ] + posts.sort(key=lambda p: p.date) + + blogpages = {} + + # Generate overview page + html = ["

Blog

"] + for page in reversed(posts): + text = page.md + if "" in text: + summary = text.split("")[-1].split( + "" + )[0] + else: + summary = text.split("## ")[0] + summary = summary.split("-->")[-1] + + # html.append("
" + page.date_and_tags_html) + html.append("
" + page.date_and_tags_html + "
") + html.append(f'

{page.title}

') + if page.thumbnail: + html.append(f"") + # html.append(f'

{page.title}

') + html.append("

" + summary + "

") + html.append(f"read more ...

") + html.append("
") + blogpages["overview"] = "\n".join(html) + + # Generate archive page + year = "" + html = ["

Archive

\n"] + for page in reversed(posts): + if page.date[:4] != year: + year = page.date[:4] + html.append(f"

{year}

") + html.append(f'{page.date}: {page.title}
') + blogpages["archive"] = "\n".join(html) + + # todo: Generate page for each tag + + return blogpages + + +def create_assets(): + """ Returns a dict of all the assets representing the website. + """ + assets = {} + + # Load all static files + for root, dirs, files in os.walk(STATIC_DIR): + for fname in files: + filename = os.path.join(root, fname) + with open(filename, "rb") as f: + assets[os.path.relpath(filename, STATIC_DIR)] = f.read() + + # Collect pages + pages = {} + for fname in os.listdir(PAGES_DIR): + if fname.lower().endswith(".md"): + name = fname.split(".")[0].lower() + with open(os.path.join(PAGES_DIR, fname), "rb") as f: + md = f.read().decode() + pages[name] = Page(name, md) + + # Collect blog posts + posts = {} + for fname in os.listdir(POSTS_DIR): + if fname.lower().endswith(".md"): + name = fname.split(".")[0].lower() + assert name not in pages, f"blog post slug not allowed: {name}" + with open(os.path.join(POSTS_DIR, fname), "rb") as f: + md = f.read().decode() + posts[name] = Page(name, md) + + # Get template + with open(os.path.join(THIS_DIR, "template.html"), "rb") as f: + html_template = f.read().decode() + + with open(os.path.join(THIS_DIR, "style.css"), "rb") as f: + css = f.read().decode() + css += "/* Pygments CSS */\n" + HtmlFormatter(style="vs").get_style_defs( + ".highlight" + ) + + # Generate posts + for page in posts.values(): + page.prepare(pages.keys()) + title = page.title + menu = create_menu(page) + html = html_template.format( + title=title, style=css, body=page.to_html(), menu=menu + ) + print("generating post", page.name + ".html") + assets[page.name + ".html"] = html.encode() + + # Generate pages + for page in pages.values(): + page.prepare(pages.keys()) + title = TITLE if page.name == "index" else TITLE + " - " + page.title + menu = create_menu(page) + html = html_template.format( + title=title, style=css, body=page.to_html(), menu=menu + ) + print("generating page", page.name + ".html") + assets[page.name + ".html"] = html.encode() + + # Generate special pages + fake_md = "" # "##index\n## archive\n## tags" + for name, html in create_blog_relatated_pages(posts).items(): + name = "blog" if name == "overview" else name + print("generating page", name + ".html") + assets[f"{name}.html"] = html_template.format( + title=TITLE, style=css, body=html, menu=create_menu(Page("", fake_md)) + ).encode() + + # Backwards compat with previous site + for page in pages.values(): + assets["pages/" + page.name + ".html"] = REDIRECT.replace( + "URL", f"/{page.name}.html" + ).encode() + + # Fix backslashes on Windows + for key in list(assets.keys()): + if "\\" in key: + assets[key.replace("\\", "/")] = assets.pop(key) + + return assets + + +def main(): + """ Main function that exports the page to the file system. + """ + # Create / clean output dir + if os.path.isdir(OUT_DIR): + shutil.rmtree(OUT_DIR) + os.mkdir(OUT_DIR) + + # Write all assets to the directory + for fname, bb in create_assets().items(): + filename = os.path.join(OUT_DIR, fname) + dirname = os.path.dirname(filename) + if not os.path.isdir(dirname): + os.makedirs(dirname) + with open(filename, "wb") as f: + f.write(bb) + + +class Page: + """ Representation of a page. It takes in markdown and produces HTML. + """ + + def __init__(self, name, markdown): + self.name = name + self.md = markdown + self.parts = [] + self.headers = [] + + self.title = name + if markdown.startswith("# "): + self.title = markdown.split("\n")[0][1:].strip() + + self.date = None + if "")[0].strip() or None + if self.date is not None: + assert ( + len(self.date) == 10 and self.date.count("-") == 2 + ), f"Weird date in {name}.md" + + self.author = None + if "")[0].strip() + + self.tags = [] + if "")[0].split(",") + ] + + self.date_and_tags_html = "" + if self.date: + self.date_and_tags_html = f"{', '.join(self.tags)}  -  {self.date}" + + self.thumbnail = None + for fname in ["thumbs/" + self.name + ".jpg"]: + if os.path.isfile(os.path.join(THIS_DIR, "static", fname)): + self.thumbnail = fname + + def prepare(self, page_names): + # Convert markdown to HTML + self.md = self._fix_links(self.md, page_names) + self.md = self._highlight(self.md) + self._split() # populates self.parts and self.headers + + def _fix_links(self, text, page_names): + """ Fix the markdown links based on the pages that we know. + """ + for n in page_names: + text = text.replace(f"]({n})", f"]({n}.html)") + text = text.replace(f"]({n}.md)", f"]({n}.html)") + return text + + def _highlight(self, text): + """ Apply syntax highlighting. + """ + lines = [] + code = [] + for i, line in enumerate(text.splitlines()): + if line.startswith("```"): + if code: + formatter = HtmlFormatter() + try: + lexer = get_lexer_by_name(code[0]) + except Exception: + lexer = get_lexer_by_name("text") + lines.append( + pygments.highlight("\n".join(code[1:]), lexer, formatter) + ) + code = [] + else: + code.append(line[3:].strip()) # language + elif code: + code.append(line) + else: + lines.append(line) + return "\n".join(lines).strip() + + def _split(self): + """ Split the markdown into parts based on sections. + Each part is either text or a tuple representing a section. + """ + text = self.md + self.parts = parts = [] + self.headers = headers = [] + lines = [] + + # Split in parts + for line in text.splitlines(): + if line.startswith(("# ", "## ", "### ", "#### ", "##### ")): + # Finish pending lines + parts.append("\n".join(lines)) + lines = [] + # Process header + level = len(line.split(" ")[0]) + title = line.split(" ", 1)[1] + title_short = title.split("(")[0].split("<")[0].strip().replace("`", "") + headers.append((level, title_short)) + parts.append((level, title_short, title)) + else: + lines.append(line) + parts.append("\n".join(lines)) + + # Now convert all text to html + for i in range(len(parts)): + if not isinstance(parts[i], tuple): + parts[i] = markdown.markdown(parts[i], extensions=[]) + "\n\n" + + def to_html(self): + htmlparts = [] + for part in self.parts: + if isinstance(part, tuple): + level, title_short, title = part + title_html = ( + title.replace("``", "`") + .replace("`", "", 1) + .replace("`", "", 1) + ) + ts = title_short.lower() + if part[0] == 1: + htmlparts.append(self.date_and_tags_html) + htmlparts.append("

%s

" % title_html) + elif part[0] == 2 and title_short: + htmlparts.append( + "".format(ts, ts) + ) + htmlparts.append("%s" % (level, title_html, level)) + htmlparts.append("") + else: + htmlparts.append("%s" % (level, title_html, level)) + else: + htmlparts.append(part) + return "\n".join(htmlparts) + + +if __name__ == "__main__": + main() + # webbrowser.open(os.path.join(OUT_DIR, "index.html")) diff --git a/pages/index.md b/pages/index.md new file mode 100644 index 0000000..4f09dd1 --- /dev/null +++ b/pages/index.md @@ -0,0 +1,72 @@ + +pygfx.org + + +## ๐Ÿ’ซ Projects + +
+

Pygfx

+ + A powerful render engine for Python

+ Repo: github.com/pygfx/pygfx
+ Docs: pygfx.readthedocs.io
+
+ +
+

wgpu-py

+ + WebGPU for Python

+ Repo: github.com/pygfx/wgpu
+ Docs: wgpu-py.readthedocs.io
+
+ +
+

Other

+ Projects that we also contribute to

+ wgpu-native
+ jupyter_rfb
+ pylinalg +
+ + +## ๐Ÿš€ Mission + +We are dedicated to bring powerful and reliable visualization to the Python world. + +We believe that wgpu is the future for graphics and bring it to Python with the wgpu-py library. On top of that, we build pygfx: a modern, versatile, and Pythonic rendering engine. + +Pygfx provides a basis on top of which a multitude of visualizations become possible. From applications to libraries, from games to plotting. + + + + +## โค๏ธ Current sponsors + +Pygfx and wgpu are free. To develop these projects we rely on [funding from our sponsors](sponsor.html). The more groups "chip in", the more time we can spend on moving the projects forwards. Recurring funding is especially welcome. + +
+

Ramona optics

+
+
https://ramonaoptics.com +
+ +
+

The Flatiron institute

+
+ https://simonsfoundation.org/flatiron/ +
+ + + +## ๐Ÿ‘ฅ Team + +
+
+ @almarklein +
+ +
+
+ @korijn +
+ diff --git a/pages/sponsor.md b/pages/sponsor.md new file mode 100644 index 0000000..c4a9b2d --- /dev/null +++ b/pages/sponsor.md @@ -0,0 +1,47 @@ + + +# Sponsoring Pygfx + + +## ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Keep pygfx independent and active + +Maintaining and growing wgpu and pygfx costs time and dedication. We rely on sponsors to maintain (and grow) the project further. +If you represent a company / group that relies on pygfx or wgpu-pu, we kindly ask for a sponsorship. That way we can keep replying to issues, review pull request, and move pygfx further. + + +## ๐ŸŽ What you get + +* Most importantly, sponsors help ensure that pygfx is actively maintained! +* Sponsors also get priority on bug reports and feature requests. +* An honorable mention on the front page of pygfx.org! +* In the top tiers, one-on-one support to help you use Pygfx to the max. + +We employ a few different [sponsorship tiers](https://github.com/sponsors/pygfx). + + +## ๐Ÿงพ Ways to sponsor Pygfx + +We provide a few ways to get funds to us. If you have questions, do not hesitate to reach out to [support@pygfx.com](mailto:support@pygfx.com)! + +### Directly + +You can sponsor us directly via *Almar Klein scientific computing*, based in The Netherlands. We can provide an invoice and you pay by bank transfer. +Incoming funds for Pygfx are received at a dedicated bank account, and insights into how the funds are spent will be published on a yearly basis. + + +### Via OpenCollective + +You can sponsor us via [https://opencollective.com/pygfx](https://opencollective.com/pygfx). These funds and how they are spent are publicly visible. + + +### Via Github + +You can also sponsor via Github's sponsor system: https://github.com/sponsors/pygfx + + +## ๐Ÿ’ฐ How funds are spent + +Sponsorship funds for Pygfx are primarily used to fund our developer time. +If we receive more funds than we can spend, the surplus acts as a buffer to create runway. If that buffer becomes large enough we plan to onboard additional developers. + + diff --git a/static/pygfx.png b/static/pygfx.png new file mode 100644 index 0000000..409abc0 Binary files /dev/null and b/static/pygfx.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..e8d525c --- /dev/null +++ b/style.css @@ -0,0 +1,259 @@ +/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ +html{line-height:1.15;-webkit-text-size-adjust:100%} +body{margin:0} +h1{font-size:2em;margin:.67em 0} +hr{box-sizing:content-box;height:0;overflow:visible} +pre{font-family:monospace,monospace;font-size:1em} +a{background-color:transparent} +abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} +b,strong{font-weight:bolder} +code,kbd,samp{font-family:monospace,monospace;font-size:1em} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sub{bottom:-.25em} +sup{top:-.5em} +img{border-style:none} +button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible} +button,select{text-transform:none} +button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button} +button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0} +button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText} +fieldset{padding:.35em .75em .625em} +legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal} +progress{vertical-align:baseline} +textarea{overflow:auto} +[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0} +[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto} +[type="search"]{-webkit-appearance:textfield;outline-offset:-2px} +[type="search"]::-webkit-search-decoration{-webkit-appearance:none} +::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} +details{display:block} +summary{display:list-item} +template{display:none} +[hidden]{display:none} + +html { + height: 100%; +} + +body { + height: 100%; + font-family: Ubuntu,"Helvetica Neue",Arial,sans-serif; + color: #404040; + font-weight: normal; + background: #fcfcfc; +} +.content { + box-sizing: border-box; + padding: 1em 1em; + width: 100%; + + position: static; + max-width: none; + margin: 0; + margin-top: 1em; + + /* + background: #fcfcfc; + */ + border-radius: 6px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} +p, li { + line-height: 150%; +} +.menu { + box-sizing: border-box; + position: static; + width: 100%; + max-width: none; + + background: #f8f8f8; + padding: 0.5em 1em; + overflow: hidden; + white-space: nowrap; + + border-radius: 6px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} + +.projectbox, .sponsorbox, .profilebox { + box-sizing: border-box; + display: inline-block; + position: relative; /* so stuff can be abs-positined inside */ + width: 100%; + background: #f1f1f1; + padding: 1em 1em; + margin: 0.5em; + border-radius: 6px; +} +.profilebox { + text-align: center; +} +.projectbox h3, .sponsorbox h3 { + color:#444; + font-size: 110%; +} +.sponsorbox img { + margin-bottom: 8px; + height: 50px; +} + +img.stars-badge { + position: absolute; + display: block; + top: 7px; + right: 7px; +} + +img.profile { + width: 100px; + height: 100px; + border-radius: 50px; +} + +@media screen and (min-width: 500px) { + .projectbox { + width: 450px; + } + .sponsorbox { + width: 450px; + } + .profilebox { + width: 150px; + } +} +@media screen and (min-width: 1300px) { + .content { + width: 1000px; + padding: 1em 1.5em; + margin-left: auto; + margin-right: 370px; + } + .absspacer { + height: 100px; + } + .menu { + position: fixed; + top: 1em; + right: 10px; + max-width: 250px; + margin-top: 0; + } +} +@media screen and (min-width: 1650px) { + .content { + margin-right: auto; + } + .menu { + left: calc(50% + 500px + 20px); + max-width: 300px; + } +} + +a:link, a:visited, a:active { + color: #36C; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.menu .header { + color: #aaa; + display: block; + border-bottom: 1px solid #ccc; +} +.menu a { + font-size: 120%; + color: #000; + line-height: 150%; + margin-left: 0.5em; +} +.menu a.current { + font-weight: bold; +} +.menu a.sub { + font-size: 90%; + /*margin-left: 1.5em;*/ +} + +.menu .ad { + box-sizing: border-box; + max-width: 270px; + white-space: normal; + text-align: center; +} +.menu .ad a { + font-size: 70%; + line-height: 100%; + margin: 0; +} +.menu .ad a.a-ad { + font-size: 95%; + text-decoration: none; +} + +a.anch:hover { + text-decoration: none; +} +a.anch:hover h2::after { + content: " \00B6"; + color: rgba(0, 0, 0, 0.3); + font-size: 80%; +} +hr { + height: 1px; + background: rgba(30, 60, 90, 0.2); + border: 0px solid #ccc; +} +.footer { + color: #888; + font-size: 80%; +} +code { + font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-size: 90%; + color: #000; + background: #fff; + padding: 1px 5px; + white-space: nowrap; + border: solid 1px #e1e4e5; +} +.highlight { /*pygments */ + font-family: dejavu sans mono,Consolas,"Andale Mono WT","Andale Mono","Lucida Console", "Courier New",Courier,monospace; + font-size: 12px; + color: #444; + background: #fff; + border: 1px solid #dddddd; + padding: 0em 1em; +} +h1, h2, h3, h4 { + color: #999; + font-family: Consolas, "DejaVu Sans Mono", Monaco, "Courier New", Courier, monospace; +} +a.header:hover { + color: #2A4; +} +h2 { + margin-top: 1.3em; + border-bottom: 1px solid rgba(20, 100, 40, 0.3); +} +h2 code, h3 code, h4 code { + color: #369; + padding-left: 0; + background: none; + border: 0px; + font-size: 85%; +} +span.post-date-tags { + float: right; + color: #888; + font-size: 80%; +} +img.thumb { + width: 128px; + height: 128px; + float: left; + margin: 0 1em 0.5em 0; + border-radius: 4px; +} diff --git a/template.html b/template.html new file mode 100644 index 0000000..51a3d4a --- /dev/null +++ b/template.html @@ -0,0 +1,36 @@ + + + + {title} + + + + + + + +
+ + + + {body} + +
+ +
+ + + +
+ +
+ + +