diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml new file mode 100644 index 0000000..40907d4 --- /dev/null +++ b/.github/workflows/publish-site.yml @@ -0,0 +1,54 @@ +name: Docs + +on: + push: + branches: ["main"] + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install CuteKit + run: pip install git+https://github.com/cute-engineering/cutekit.git@0.7.4 + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + + - name: Build with cat + run: | + pip install -r meta/plugins/requirements.txt + python -m cutekit cat build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: .cutekit/build/site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b6d32c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cutekit +.mypy_cache diff --git a/license.md b/license.md new file mode 100644 index 0000000..00040ae --- /dev/null +++ b/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Cute Engineering + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/meta/plugins/__init__.py b/meta/plugins/__init__.py new file mode 100644 index 0000000..84e9744 --- /dev/null +++ b/meta/plugins/__init__.py @@ -0,0 +1,178 @@ +from cutekit import cli, const, shell + +import http.server +import os +from pathlib import Path +import markdown +import dataclasses as dt +from dataclasses_json import DataClassJsonMixin + +from cutekit import const, shell, jexpr + +CAT = "ᓚ₍ ^. .^₎" +DEFAULT_STYLE_PATH = __file__.replace("__init__.py", "default.css") + +SITE_DIR = os.path.join(const.META_DIR, "site") +SITE_BUILD_DIR = os.path.join(const.BUILD_DIR, "site") + + +# MARK: Model ------------------------------------------------------------------ + + +def readFile(path: str) -> str: + with open(path, "r") as f: + return f.read() + + +def writeFile(path: str, content: str): + with open(path, "w") as f: + f.write(content) + + +def fixupLinks(html: str) -> str: + return html.replace('.md"', '.html"').replace(".md#", ".html#") + + +@dt.dataclass +class Site(DataClassJsonMixin): + title: str = dt.field() + header: str | None = dt.field(default=None) + favicon: str = dt.field(default="🐱") + navbar: str = dt.field(default="") + footer: str = dt.field(default="") + + path: str = dt.field(default="") + + @staticmethod + def load() -> "Site": + siteFile = os.path.join(SITE_DIR, "site.json") + siteJson = jexpr.include(Path(siteFile)) + site = Site.from_dict(siteJson) + site.path = siteFile + return site + + def renderFavicon(self) -> str: + svg = f"{self.favicon}" + urlEscape = str.maketrans({"#": "%23", "<": "%3C", ">": "%3E"}) + svgEscaped = svg.translate(urlEscape) + return f"data:image/svg+xml,{svgEscaped}" + + def renderHeader(self) -> str: + return f'

{self.header or self.title}

' + + def renderAll(self, out: str): + styleFile = os.path.join(SITE_DIR, "style.css") + if not os.path.exists(styleFile): + styleFile = DEFAULT_STYLE_PATH + + style = readFile(styleFile) + + md = markdown.Markdown(extensions=["meta"]) + files = shell.find(SITE_DIR) + for file in files: + if file.endswith(".json"): + continue + + output = os.path.join(out, file.replace(SITE_DIR + "/", "")) + if not file.endswith(".md"): + shell.mkdir(os.path.dirname(output)) + shell.cp(file, out) + continue + + output = output.replace(".md", ".html") + + print(f"Rendering {file} -> {output}") + + shell.mkdir(os.path.dirname(output)) + + mdContent = readFile(path=file) + htmlContent = md.convert(mdContent) + + title = md.Meta.get("title", [""])[0] + if title: + title = f"{title} - {self.title}" + else: + title = self.title + + htmlContent = f""" + + + + + + {title} + + + + +
+ {self.renderHeader()} + +
+
+ {htmlContent} +
+ + + +""" + + writeFile(output, fixupLinks(htmlContent)) + + +# MARK: Public interface ------------------------------------------------------- + + +@cli.command(None, "cat", "Tiny site generator") +def _(): + print(CAT) + + +@cli.command("b", "cat/build", "Build the site") +def _() -> None: + shell.rmrf(SITE_BUILD_DIR) + shell.mkdir(SITE_BUILD_DIR) + site = Site.load() + site.renderAll(SITE_BUILD_DIR) + + +@cli.command("c", "cat/clean", "Clean the site") +def _(): + shell.rmrf(SITE_BUILD_DIR) + + +@cli.command("s", "cat/serve", "Serve the site") +def _(): + shell.rmrf(SITE_BUILD_DIR) + shell.mkdir(SITE_BUILD_DIR) + site = Site.load() + site.renderAll(SITE_BUILD_DIR) + + os.chdir(SITE_BUILD_DIR) + shell.exec("python3", "-m", "http.server") + + +@cli.command("e", "cat/init", "Initialize the site") +def _(): + shell.mkdir(SITE_DIR) + writeFile( + os.path.join(SITE_DIR, "site.json"), + """ +{ + "favicon": "🐱", + "title": "Cat", + "header": "ᓚ₍ ^. .^₎", + "navbar": "[Home](/)", + "footer": "Built with [ᓚ₍ ^. .^₎](https://github.com/cute-engineering/cat)" +} + """, + ) + + writeFile( + os.path.join(SITE_DIR, "index.md"), + """ +This is the home page of the site. You can edit this file to change the content of the home page. +""", + ) diff --git a/meta/plugins/__pycache__/__init__.cpython-311.pyc b/meta/plugins/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a8ea2a3 Binary files /dev/null and b/meta/plugins/__pycache__/__init__.cpython-311.pyc differ diff --git a/meta/plugins/__pycache__/cat.cpython-311.pyc b/meta/plugins/__pycache__/cat.cpython-311.pyc new file mode 100644 index 0000000..2be224e Binary files /dev/null and b/meta/plugins/__pycache__/cat.cpython-311.pyc differ diff --git a/meta/plugins/default.css b/meta/plugins/default.css new file mode 100644 index 0000000..ec6a786 --- /dev/null +++ b/meta/plugins/default.css @@ -0,0 +1,159 @@ +:root { + --width: 720px; + --font-main: Verdana, sans-serif; + --font-secondary: Verdana, sans-serif; + --font-scale: 1em; + --background-color: #fff; + --heading-color: #222; + --text-color: #444; + --link-color: #3273dc; + --visited-color: #8b6fcb; + --code-background-color: #f2f2f2; + --code-color: #222; + --blockquote-color: #222; +} + +@media (prefers-color-scheme: dark) { + :root { + --background-color: #01242e; + --heading-color: #eee; + --text-color: #ddd; + --link-color: #8cc2dd; + --visited-color: #8b6fcb; + --code-background-color: #000; + --code-color: #ddd; + --blockquote-color: #ccc; + } +} + +body { + font-family: var(--font-secondary); + font-size: var(--font-scale); + margin: auto; + padding: 20px; + max-width: var(--width); + text-align: left; + background-color: var(--background-color); + word-wrap: break-word; + overflow-wrap: break-word; + line-height: 1.5; + color: var(--text-color); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-main); + color: var(--heading-color); +} + +a { + color: var(--link-color); + cursor: pointer; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +nav a { + margin-right: 8px; +} + +strong, +b { + color: var(--heading-color); +} + +button { + margin: 0; + cursor: pointer; +} + +time { + font-family: monospace; + font-style: normal; + font-size: 15px; +} + +main { + line-height: 1.6; +} + +table { + width: 100%; +} + +hr { + border: 0; + border-top: 1px dashed; +} + +img { + max-width: 100%; +} + +code { + font-family: monospace; + padding: 2px; + background-color: var(--code-background-color); + color: var(--code-color); + border-radius: 3px; +} + +blockquote { + border-left: 1px solid #999; + color: var(--code-color); + padding-left: 20px; + font-style: italic; +} + +footer { + padding: 25px 0; + text-align: center; +} + +.title:hover { + text-decoration: none; +} + +.title h1 { + font-size: 1.5em; +} + +.inline { + width: auto !important; +} + +.highlight, +.code { + padding: 1px 15px; + background-color: var(--code-background-color); + color: var(--code-color); + border-radius: 3px; + margin-block-start: 1em; + margin-block-end: 1em; + overflow-x: auto; +} + +/* blog post list */ +ul.blog-posts { + list-style-type: none; + padding: unset; +} + +ul.blog-posts li { + display: flex; +} + +ul.blog-posts li span { + flex: 0 0 130px; +} + +ul.blog-posts li a:visited { + color: var(--visited-color); +} diff --git a/meta/plugins/requirements.txt b/meta/plugins/requirements.txt new file mode 100644 index 0000000..9b2aa88 --- /dev/null +++ b/meta/plugins/requirements.txt @@ -0,0 +1 @@ +Markdown~=3.7 diff --git a/meta/site/blog/index.md b/meta/site/blog/index.md new file mode 100644 index 0000000..2a39f81 --- /dev/null +++ b/meta/site/blog/index.md @@ -0,0 +1,2 @@ +# Posts + - [Initial Release](initial-release.md) diff --git a/meta/site/blog/initial-release.md b/meta/site/blog/initial-release.md new file mode 100644 index 0000000..9c44a0a --- /dev/null +++ b/meta/site/blog/initial-release.md @@ -0,0 +1 @@ +It's cat's first release! 🎉 diff --git a/meta/site/cat.jpg b/meta/site/cat.jpg new file mode 100644 index 0000000..4d97f66 Binary files /dev/null and b/meta/site/cat.jpg differ diff --git a/meta/site/index.md b/meta/site/index.md new file mode 100644 index 0000000..ca5a532 --- /dev/null +++ b/meta/site/index.md @@ -0,0 +1,3 @@ +**Cat** is a tiny static site generator for CuteKit. + +![Cat writting the blog](cat.jpg) diff --git a/meta/site/site.json b/meta/site/site.json new file mode 100644 index 0000000..10bd41b --- /dev/null +++ b/meta/site/site.json @@ -0,0 +1,7 @@ +{ + "favicon": "🐱", + "title": "Cat", + "header": "ᓚ₍ ^. .^₎", + "navbar": "[Home](/) [Blog](/blog) [GitHub](https://github.com/cute-engineering/cat)", + "footer": "© 2024 Cute Engineering" +} diff --git a/project.json b/project.json new file mode 100644 index 0000000..7566423 --- /dev/null +++ b/project.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.project.v1", + "id": "cute-engineeering/cat", + "type": "project", + "description": "A tiny static site generator for CuteKit projects." +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..28604de --- /dev/null +++ b/readme.md @@ -0,0 +1,26 @@ +# 🐱 Cat + +A tiny static site generator for CuteKit projects. + +## Features + + - Nothing fancy + - purrs like a kitten + - Easy to use with CuteKit + +## Installation + +Just add the following piece of json to your `project.json` file: + +```json +{ + "extern": { + "cute-engineering/cat": { + "git": "https://github.com/cute-engineering/cat.git", + "tag": "v0.7.0" + }, + } +} +``` + +Then run `cutekit model init` to install the package and `cutekit cat init` to initialize the site.