From 0c6b84a87235e307325df2d7f79a43bcb649085a Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 12 Dec 2025 13:13:14 -0500 Subject: [PATCH] feat: Initial commit. --- .dockerignore | 2 + .editorconfig | 7 + .gitattributes | 5 + .gitea/workflows/ci.yaml | 62 ++++ .gitea/workflows/preview.yaml | 61 ++++ .gitignore | 16 + .prettierrc.yaml | 2 + .swiftformat | 11 + Caddyfile | 6 + LICENSE | 21 ++ LICENSE-CONTENT | 407 ++++++++++++++++++++++++ Package.swift | 26 ++ README.md | 19 ++ Sources/Site/Metadata.swift | 27 ++ Sources/Site/Section.swift | 9 + Sources/Site/run.swift | 26 ++ Sources/Site/templates/BaseLayout.swift | 53 +++ Sources/Site/templates/RenderPage.swift | 36 +++ content/index.md | 7 + content/static/style.css | 10 + content/table.md | 9 + docker/Dockerfile | 57 ++++ package.json | 20 ++ pnpm-lock.yaml | 288 +++++++++++++++++ 24 files changed, 1187 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/preview.yaml create mode 100644 .gitignore create mode 100644 .prettierrc.yaml create mode 100644 .swiftformat create mode 100644 Caddyfile create mode 100644 LICENSE create mode 100644 LICENSE-CONTENT create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/Site/Metadata.swift create mode 100644 Sources/Site/Section.swift create mode 100644 Sources/Site/run.swift create mode 100644 Sources/Site/templates/BaseLayout.swift create mode 100644 Sources/Site/templates/RenderPage.swift create mode 100644 content/index.md create mode 100644 content/static/style.css create mode 100644 content/table.md create mode 100644 docker/Dockerfile create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c1c0716 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +# .build/* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7cfbe01 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 2 +tab_width = 2 +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..99967f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.avif filter=lfs dif=lfs merg=lfs -text +*.webp filter=lfs dif=lfs merg=lfs -text + diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..353a443 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,62 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ['main'] + workflow_dispatch: + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, +# and a name for the Docker image that this workflow builds. +env: + REGISTRY: git.housh.dev + IMAGE_NAME: ${{ gitea.repository }} + DOCKERFILE: docker/Dockerfile + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account + # and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.CONTAINER_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels + # that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a + # subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha + type=raw,value=latest + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ env.DOCKERFILE }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/.gitea/workflows/preview.yaml b/.gitea/workflows/preview.yaml new file mode 100644 index 0000000..d2019c5 --- /dev/null +++ b/.gitea/workflows/preview.yaml @@ -0,0 +1,61 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + pull_request: {} + workflow_dispatch: {} + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, +# and a name for the Docker image that this workflow builds. +env: + REGISTRY: git.housh.dev + IMAGE_NAME: ${{ gitea.repository }} + DOCKERFILE: docker/Dockerfile + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account + # and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.CONTAINER_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels + # that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a + # subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha + type=raw,value=preview + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ env.DOCKERFILE }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0a5a6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +/build +/.build +/.swiftpm +/*.xcodeproj +.publish +Output +public/* +.hugo_build.lock +deploy +node_modules +env +Package.resolved + +# Local Netlify folder +.netlify diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..eaae201 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,2 @@ +printWidth: 100 +proseWrap: always diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..08f338e --- /dev/null +++ b/.swiftformat @@ -0,0 +1,11 @@ +--self init-only +--indent 2 +--ifdef indent +--trimwhitespace always +--wraparguments before-first +--wrapparameters before-first +--wrapcollections preserve +--wrapconditions after-first +--typeblanklines preserve +--commas inline +--stripunusedargs closure-only diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..5dfe6c0 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,6 @@ +:80 { + route /* { + root * /app + file_server + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60c3117 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Michael Housh + +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/LICENSE-CONTENT b/LICENSE-CONTENT new file mode 100644 index 0000000..c657cab --- /dev/null +++ b/LICENSE-CONTENT @@ -0,0 +1,407 @@ +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..ef17401 --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SagaTable", + platforms: [.macOS(.v12)], + dependencies: [ + .package(url: "https://github.com/loopwerk/Saga", from: "2.0.0"), + .package(url: "https://github.com/loopwerk/SagaParsleyMarkdownReader", from: "1.0.0"), + .package(url: "https://github.com/loopwerk/SagaSwimRenderer", from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "Site", + dependencies: [ + .product(name: "Saga", package: "Saga"), + .product(name: "SagaParsleyMarkdownReader", package: "SagaParsleyMarkdownReader"), + .product(name: "SagaSwimRenderer", package: "SagaSwimRenderer"), + ] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d56eeb3 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# mhoush.com + +

+ avatar +

+ +This repository hosts the site files for [mhoush.com](https://mhoush.com) + +## Getting Started + +1. `brew install git-lfs just gd pnpm` +1. `git clone git@github.com/m-housh/mhoush.com.git` +1. `cd mhoush.com` +1. `just run` + +## License + +The source code for the site generation is licensed under MIT, however the content / articles are licensed under Creative Commons (CC BY-NC) +4.0. diff --git a/Sources/Site/Metadata.swift b/Sources/Site/Metadata.swift new file mode 100644 index 0000000..bff4f87 --- /dev/null +++ b/Sources/Site/Metadata.swift @@ -0,0 +1,27 @@ +import Foundation +import Saga + +/// Represents constants about the site. +enum SiteMetadata { + #if DEBUG + static let url = URL(string: "http://localhost:8080")! + #else + static let url = URL(string: "https://mhoush.com")! + #endif + static let name = "mhoush" + static let author = "Michael Housh" + /// Summary used for metadata / twitter card for home page, + /// also displayed at bottom of articles. + static let summary = """ + Test saga tables. + """ + /// The default twitter image when linking to home page. + static let twitterImage = "/static/images/home-twitter-image.png" +} + +/// Represents valid metadata for the files that are not an `article`. +struct PageMetadata: Metadata { + + /// The section of the website for the file. + let section: String? +} diff --git a/Sources/Site/Section.swift b/Sources/Site/Section.swift new file mode 100644 index 0000000..c556fad --- /dev/null +++ b/Sources/Site/Section.swift @@ -0,0 +1,9 @@ +/// Represents different sections of the website. +/// +/// This is used to render base layouts appropriately for the given section. +enum Section: String { + /// The home page of the site. + case home + /// The articles / blog posts of the site. + case table +} diff --git a/Sources/Site/run.swift b/Sources/Site/run.swift new file mode 100644 index 0000000..7e4ee81 --- /dev/null +++ b/Sources/Site/run.swift @@ -0,0 +1,26 @@ +import Foundation +import HTML +import PathKit +@preconcurrency import Saga +import SagaParsleyMarkdownReader +import SagaSwimRenderer + +@main +struct Run { + static func main() async throws { + try await Saga(input: "content", output: "deploy") + // All the remaining markdown files will be parsed to html, + // using the default EmptyMetadata as the Item's metadata type. + .register( + metadata: PageMetadata.self, + readers: [.parsleyMarkdownReader], + itemWriteMode: .keepAsFile, // need to keep 404.md as 404.html, not 404/index.html + writers: [.itemWriter(swim(renderPage))] + ) + // Run the steps we registered above + .run() + // All the remaining files that were not parsed to markdown, so for example images, raw html files and css, + // are copied as-is to the output folder. + .staticFiles() + } +} diff --git a/Sources/Site/templates/BaseLayout.swift b/Sources/Site/templates/BaseLayout.swift new file mode 100644 index 0000000..78b11eb --- /dev/null +++ b/Sources/Site/templates/BaseLayout.swift @@ -0,0 +1,53 @@ +import Foundation +import HTML + +/// The base page layout used to render the different sections of the website. +/// +/// - Parameters: +/// - conocicalURL: The url for the page. +/// - section: The section of the page. +/// - title: The page title. +/// - rssLink: A prefix for generating an rss feed for the page (generally only used for articles). +/// - extraHeader: Any extra items to be placed in the `head` of the html. +func baseLayout( + canocicalURL: String, + section: Section, + title pageTitle: String, + rssLink: String = "", + extraHeader: NodeConvertible = Node.fragment([]), + @NodeBuilder children: () -> NodeConvertible +) -> Node { + return [ + .documentType("html"), + html(lang: "en-US") { + generateHeader(pageTitle, extraHeader) + body(class: "bg-page text-white pb-5 font-avenir \(section.rawValue)") { + div(class: "content") { + children() + } + footer() + } + }, + ] +} + +private func footer() -> Node { + div(class: "site-footer text-gray gray-links border-t border-light text-center pt-6 mt-8 text-sm") + { + p { + "Copyright © Michael Housh 2023-\(Date().description.prefix(4))." + } + p { + "Built in Swift using" + a(href: "https://github.com/loopwerk/Saga", rel: "nofollow", target: "_blank") { "Saga" } + } + } +} + +private func generateHeader(_ pageTitle: String, _ extraHeader: NodeConvertible) -> Node { + head { + meta(charset: "utf-8") + title { SiteMetadata.name + ": \(pageTitle)" } + link(href: "/static/style.css", rel: "stylesheet") + } +} diff --git a/Sources/Site/templates/RenderPage.swift b/Sources/Site/templates/RenderPage.swift new file mode 100644 index 0000000..ba76382 --- /dev/null +++ b/Sources/Site/templates/RenderPage.swift @@ -0,0 +1,36 @@ +import HTML +import Saga + +func renderPage(context: ItemRenderingContext) -> Node { + let section = Section(rawValue: context.item.metadata.section ?? "") + assert(section != nil) + + return baseLayout( + canocicalURL: context.item.url, + section: section!, + title: context.item.title + ) { + switch section { + case .home: + renderHome(body: context.item.body) + default: + renderNonHome(body: context.item.body) + } + } +} + +func renderHome(body: String) -> Node { + div { + Node.raw(body) + } +} + +func renderNonHome(body: String) -> Node { + div { + article { + div { + Node.raw(body) + } + } + } +} diff --git a/content/index.md b/content/index.md new file mode 100644 index 0000000..800ad68 --- /dev/null +++ b/content/index.md @@ -0,0 +1,7 @@ +--- +section: home +--- + +# Home + +This is content without a table. diff --git a/content/static/style.css b/content/static/style.css new file mode 100644 index 0000000..d071abe --- /dev/null +++ b/content/static/style.css @@ -0,0 +1,10 @@ +body { + font-family: Helvetica; +} + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} diff --git a/content/table.md b/content/table.md new file mode 100644 index 0000000..4871425 --- /dev/null +++ b/content/table.md @@ -0,0 +1,9 @@ +--- +section: table +--- + +# Table + +| Header 1 | Header 2 | +| --------- | --------- | +| A content | B content | diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..b0222ca --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# ================================================== +# Build Swift Image +# ================================================== +FROM docker.io/swift:6.2-noble AS build + +# Install OS updates +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y + +WORKDIR /build +# First just resolve dependencies. +COPY ./Package.* ./ +RUN --mount=type=cache,target=/build/.build swift package resolve + +# Copy entire repo into container +COPY . . + +# Remove all the backtrace memory warnings. +ENV SWIFT_BACKTRACE=enable=no + +# Build the static site. +RUN --mount=type=cache,target=/build/.build swift run + +# ================================================== +# Build HTML Minify +# ================================================== + +FROM docker.io/node:23-alpine AS css + +WORKDIR /build + +RUN npm install -g html-minifier + +COPY . . +COPY --from=build /build/deploy ./deploy + +RUN html-minifier --collapse-whitespace --input-dir ./deploy --file-ext html --output-dir deploy + +# ================================================== +# Run Image +# ================================================== +FROM docker.io/caddy:latest + +WORKDIR /app + +COPY --from=css /build/deploy . +COPY Caddyfile /etc/caddy/Caddyfile + +VOLUME /app + +RUN /usr/bin/caddy fmt --overwrite /etc/caddy/Caddyfile + +EXPOSE 80 + +CMD ["/usr/bin/caddy", "run", "--config", "/etc/caddy/Caddyfile"] + diff --git a/package.json b/package.json new file mode 100644 index 0000000..f004bc8 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "mhoush.com", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "css-watch": "npx @tailwindcss/cli -i ./content/static/input.css -o ./content/static/output.css --minify --watch", + "css-build": "npx @tailwindcss/cli -i ./content/static/input.css -o ./content/static/output.css --minify" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "tailwindcss": "^4.1" + }, + "dependencies": { + "sass": "^1.85.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..34f7f67 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,288 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + sass: + specifier: ^1.85.0 + version: 1.85.0 + devDependencies: + tailwindcss: + specifier: ^4.1 + version: 4.1.17 + +packages: + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + sass@1.85.0: + resolution: {integrity: sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==} + engines: {node: '>=14.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + +snapshots: + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + optional: true + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + detect-libc@1.0.3: + optional: true + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + optional: true + + immutable@5.0.3: {} + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-number@7.0.0: + optional: true + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + optional: true + + node-addon-api@7.1.1: + optional: true + + picomatch@2.3.1: + optional: true + + readdirp@4.1.2: {} + + sass@1.85.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.0.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + source-map-js@1.2.1: {} + + tailwindcss@4.1.17: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + optional: true