Repository: rishikanthc/markopolis Branch: main Commit: ed4147787f89 Files: 123 Total size: 177.6 KB Directory structure: gitextract_oakw8baj/ ├── .dockerignore ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── components.json ├── docker-compose.yaml ├── docs/ │ ├── Changelog/ │ │ └── 3.0.0.md │ ├── Development/ │ │ ├── APIs.md │ │ ├── Markdown Rendering.md │ │ └── components.md │ ├── Markdown Syntax.md │ ├── installation.md │ ├── introduction.md │ ├── roadmap.md │ └── usage.md ├── eslint.config.js ├── package.json ├── postcss.config.js ├── src/ │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib/ │ │ ├── components/ │ │ │ ├── FileTree.svelte │ │ │ ├── GraphScene.svelte │ │ │ ├── LoginForm.svelte │ │ │ ├── MDGraph.svelte │ │ │ ├── MDsvexRenderer.svelte │ │ │ ├── MarkdownGraph.svelte │ │ │ ├── Scene.svelte │ │ │ ├── SearchComponent.svelte │ │ │ ├── Sidebar.svelte │ │ │ ├── TagBar.svelte │ │ │ ├── TopBar.svelte │ │ │ ├── award.svelte │ │ │ ├── schema.ts │ │ │ └── ui/ │ │ │ ├── button/ │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── card/ │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ ├── card.svelte │ │ │ │ └── index.ts │ │ │ ├── dialog/ │ │ │ │ ├── dialog-content.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-portal.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ └── index.ts │ │ │ ├── form/ │ │ │ │ ├── form-button.svelte │ │ │ │ ├── form-description.svelte │ │ │ │ ├── form-element-field.svelte │ │ │ │ ├── form-field-errors.svelte │ │ │ │ ├── form-field.svelte │ │ │ │ ├── form-fieldset.svelte │ │ │ │ ├── form-label.svelte │ │ │ │ ├── form-legend.svelte │ │ │ │ └── index.ts │ │ │ ├── input/ │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── label/ │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── scroll-area/ │ │ │ │ ├── index.ts │ │ │ │ ├── scroll-area-scrollbar.svelte │ │ │ │ └── scroll-area.svelte │ │ │ └── separator/ │ │ │ ├── index.ts │ │ │ └── separator.svelte │ │ ├── highlightCode.ts │ │ ├── index.ts │ │ ├── md.ts │ │ ├── pbStore.ts │ │ ├── pocketbase.ts │ │ ├── remark-plugins/ │ │ │ ├── footNotes.js │ │ │ ├── highlightSyn.js │ │ │ ├── imgRel.js │ │ │ ├── mermaidDiag.js │ │ │ ├── obsidianImage.js │ │ │ └── remarkTags.ts │ │ ├── server/ │ │ │ └── auth.ts │ │ ├── stores/ │ │ │ └── sidebarStore.ts │ │ └── utils.ts │ ├── routes/ │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── [...post].md/ │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── about/ │ │ │ └── +page.svelte │ │ ├── api/ │ │ │ ├── backlinks/ │ │ │ │ └── +server.ts │ │ │ ├── graph/ │ │ │ │ └── +server.ts │ │ │ ├── hello/ │ │ │ │ └── +server.ts │ │ │ ├── img/ │ │ │ │ └── [...path]/ │ │ │ │ └── +server.ts │ │ │ ├── links/ │ │ │ │ └── +server.ts │ │ │ ├── ls/ │ │ │ │ └── +server.ts │ │ │ ├── search/ │ │ │ │ └── +server.ts │ │ │ ├── tags/ │ │ │ │ └── +server.ts │ │ │ └── upload/ │ │ │ └── +server.ts │ │ ├── login/ │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── success/ │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── publications/ │ │ │ └── +page.svelte │ │ └── tags/ │ │ └── [tag]/ │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── writing/ │ ├── +page.server.ts │ ├── +page.svelte │ ├── [...dir]/ │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── [...post].md/ │ ├── +page.server.ts │ └── +page.svelte ├── start.sh ├── start_services.sh ├── static/ │ ├── fonts/ │ │ └── Lombok.otf │ └── fonts.css ├── supervisord.conf ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # flyctl launch added from .gitignore # Byte-compiled / optimized / DLL files **/__pycache__ **/*.py[cod] **/*$py.class **/stdout # C extensions **/*.so # Distribution / packaging **/.Python **/build **/develop-eggs **/dist **/downloads **/eggs **/.eggs **/lib64 **/parts **/sdist **/var **/wheels **/share/python-wheels **/*.egg-info **/.installed.cfg **/*.egg **/MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. **/*.manifest **/*.spec # Installer logs **/pip-log.txt **/pip-delete-this-directory.txt # Unit test / coverage reports **/htmlcov **/.tox **/.nox **/.coverage **/.coverage.* **/.cache **/nosetests.xml **/coverage.xml **/*.cover **/*.py,cover **/.hypothesis **/.pytest_cache **/cover # Translations **/*.mo **/*.pot # Django stuff: **/*.log **/local_settings.py **/db.sqlite3 **/db.sqlite3-journal # Flask stuff: **/instance **/.webassets-cache # Scrapy stuff: **/.scrapy # Sphinx documentation **/docs/_build # PyBuilder **/.pybuilder **/target # Jupyter Notebook **/.ipynb_checkpoints # IPython **/profile_default **/ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control **/.pdm.toml **/.pdm-python **/.pdm-build # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm **/__pypackages__ # Celery stuff **/celerybeat-schedule **/celerybeat.pid # SageMath parsed files **/*.sage.py # Environments **/.env **/.venv **/env **/venv **/ENV **/env.bak **/venv.bak # Spyder project settings **/.spyderproject **/.spyproject # Rope project settings **/.ropeproject # mkdocs documentation site # mypy **/.mypy_cache **/.dmypy.json **/dmypy.json # Pyre type checker **/.pyre # pytype static type analyzer **/.pytype # Cython debug symbols **/cython_debug # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # flyctl launch added from .pytest_cache/.gitignore # Created by pytest automatically. .pytest_cache/**/* # flyctl launch added from .ruff_cache/.gitignore # Automatically created by ruff. .ruff_cache/**/* fly.toml ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class stdout # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-ast - id: check-merge-conflict - id: check-case-conflict - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.5.1 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format # - repo: local # hooks: # - id: pytest # name: pytest # entry: pytest # language: system # types: [python] # pass_filenames: false ================================================ FILE: Dockerfile ================================================ FROM node:22.9.0-alpine3.20 ARG POCKETBASE_ADMIN_EMAIL ARG POCKETBASE_ADMIN_PASSWORD ARG POCKETBASE_URL ARG TITLE ARG API_KEY ARG CAP1 ARG CAP2 ARG CAP3 # Set environment variables to be overridden at runtime ENV POCKETBASE_ADMIN_EMAIL=$POCKETBASE_ADMIN_EMAIL ENV POCKETBASE_ADMIN_PASSWORD=$POCKETBASE_ADMIN_PASSWORD ENV POCKETBASE_URL=$POCKETBASE_URL ENV TITLE=$TITLE ENV API_KEY=$API_KEY ENV CAP1=$CAP1 ENV CAP2=$CAP2 ENV CAP3=$CAP3 # Install required packages RUN apk update && apk add --no-cache \ unzip \ curl # download and unzip PocketBase ADD https://github.com/pocketbase/pocketbase/releases/download/v0.22.21/pocketbase_0.22.21_linux_amd64.zip /tmp/pb.zip RUN unzip /tmp/pb.zip -d /pb/ # create PocketBase data directory RUN mkdir -p /pb/pb_data # COPY start.sh /pb/start.sh # start PocketBase in a background process to set up the database # RUN chmod +x /pb/start.sh # uncomment to copy the local pb_migrations dir into the image # COPY ./pb_migrations /pb/pb_migrations # uncomment to copy the local pb_hooks dir into the image # COPY ./pb_hooks /pb/pb_hooks WORKDIR /app COPY . . COPY start_services.sh /app/start.sh RUN npm ci # RUN /pb/start.sh EXPOSE 3000 8080 # start PocketBase CMD ["/bin/sh", "/app/start.sh"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Rishikanth Chandrasekaran 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. ================================================ FILE: README.md ================================================ ## Introduction Hi, I’m [Rishikanth](https://rishikanth.me), and I’m excited to introduce you to Markopolis! It’s a web app and API server I built that lets you easily share your Markdown notes as websites while giving you full control to interact with and manage your Markdown files via a powerful API. Just point Markopolis to a folder with your Markdown files, and it’ll handle the rest. The idea is to help you create your own tools and features around your notes without being tied down by proprietary systems. It’s completely open-source and free under the MIT License. Check out the [GitHub repo](https://github.com/rishikanthc/markopolis) and start exploring! **TLDR:** Self-hosted Obsidian publish with an API to extend functionality. ## Features - **Easy setup** Extremely simple to deploy and use - **Easy publish** Publish notes online with a single command - **Markdown API interface** Interact with aspecs of markdown using REST APIs - **Extensible** Extendable using exposed APIs - **Develop your own frontend** You can use the api calls to get every section of markdown files to design your own frontend - **Instand rendering** Article is available online as soon as ypu publish - **Full text search** Fuzzy search across your entire notes vault - **Obsidian markdown flavor** Maintains compatibility with obsidian markdown syntax. Supports callouts, equations, code highlighting etc. - **Dark & Light modes** Supports toggling between light and dark themes - **Easy maintenance** Requires very little to no maintenance - **Docker support** Available as docker images to self host and lots more to come. Checkout the [roadmap](https://markopolis.app/roadmap) page for planned features. ## Demo The documentation [website](https://markopolis.app) is hosted using Markopolis and is a live demo. These notes are used to demonstrate the various aspects of Markopolis. Checkout the [Markdown Syntax](https://markopolis.app/Markdown%20Syntax.md) page for a full showcase of all supported markdown syntax. Thank you for considering Markopolis for your Markdown note-sharing needs! If you like the project considering starring the repository. ## Versioning I try to follow semantic versioning as much as possible. However, I have still not streamlined the process yet, so please bear with me if there are any mishaps. v2.0.0 achieves code separation between backend and frontend because of which I had to fast forward the docker versioning to match the python package. Going forward I'll try to avoid such mishaps and I'll be maintaining a detailed changelog at [changelog](https://markopolis.app/changelog). This is my first open-source project and I'm excited to scale it well. I started building this mostly out of my personal need, but if there's public interest I'm more than happy to accept feature requests and contributions. Any and all feedback is welcome. This project will always be open-source and maintained as I rely on it for my own notes system. If you like the project please don't forget to star the [github repo](https://github.com/rishikanthc/markopolis.git). ## Installation Installing Markopolis involves two steps. First deploying the server. Second installing the CLI tool. The CLI tool provides a utility command to upload your markdown files to the server. The articles are published as soon as this command is run. ## Step 1: Server installation We will be using Docker for deploying Markopolis. Create a docker-compose and configure environment variables. Make sure to generate and add a secure `API_KEY`. Allocate persistent storage for the Markdown files. Next create a `docker-compose.yaml` file with the following: ```yaml version: '3.8' services: markopolis: image: ghcr.io/rishikanthc/markopolis:latest ports: - "8080:8080" - "3000:3000" environment: - POCKETBASE_URL=http://127.0.0.1:8080 - API_KEY=test - POCKETBASE_ADMIN_EMAIL=admin@admin.com - POCKETBASE_ADMIN_PASSWORD=password - TITLE=Markopolis - CAP1=caption1 - CAP2=caption2 - CAP3=caption3 volumes: - ./pb_data:/app/db ``` Now you can deploy Markopolis by running `docker-compse up -d` Parameter | Description -- | -- POCKETBASE_URL | **DO NOT Change this** POCKETBASE_ADMIN_EMAIL | The admin account email for the database POCKETBASE_ADMIN_PASSWORD | The admin account password TITLE | SITE TITLE API_KEY | For security, most of the API endpoints are protected by an API key. Make sure to use a secure API key and don't share it publicly. CAP1 | Caption 1, text that appears below the site title CAP2 | Caption 2 CAP3 | Caption 3 ## STEP 2: Local installation I highly recommend configuring a virtual environment for python to keep your environment clean and and prevent any dependency issues. Below I detail the steps to do this using Conda or pip. If you are familar with this feel free to skip to the package installation section. > [!info] > You need to have python version >= 3.12 ### Setting up a virtual environment You can use either `pip` or `conda` to do this. If you are using `pip` simply run ```bash python3.12 -m venv ``` Replace `` with your desired virtual environment name. You can then activate the virtual environment using: ```bash source ``` For conda, you can use ```bash conda create -n python==3.12 ``` and activate it with ```bash conda activate ``` ### Package installation Simply install the markopolis python package using your preferred package manager. **pip:** ```bash pip install markopolis ``` ### Configuration Set the environment variables `MARKOPOLIS_DOMAIN` and `MARKOPOLIS_API` **bash or zsh (temporarily for current session)** ```bash export MARKOPOLIS_DOMAIN=https://markopolis.example.com ``` **bash or zsh (permanently for all sessions)** ```bash echo 'export MARKOPOLIS_DOMAIN=https://markopolis.example.com' >> ~/.zshrc echo 'export MARKOPOLIS_DOMAIN=https://markopolis.example.com' >> ~/.bashrc source ~/.zshrc source ~/.bashrc ``` **fish (temporarily for current session)** ```fish set -x MARKOPOLIS_DOMAIN https://markopolis.example.com ``` **fish (permanently for all sessions)** ```fish echo 'set -x MARKOPOLIS_DOMAIN "https://markopolis.example.com"' >> ~/.config/fish/config.fish source ~/.config/fish/config.fish ``` For more information on how to use Markopolis checkout the [Markopolis](https://markopolis.app) website. If you like this project please considering starring it. ================================================ FILE: components.json ================================================ { "$schema": "https://shadcn-svelte.com/schema.json", "style": "new-york", "tailwind": { "config": "tailwind.config.ts", "css": "src/app.css", "baseColor": "neutral" }, "aliases": { "components": "$lib/components", "utils": "$lib/utils" }, "typescript": true } ================================================ FILE: docker-compose.yaml ================================================ services: markopolis: image: ghcr.io/rishikanthc/markopolis:3.0.0 ports: - "8080:8080" - "3000:3000" environment: - POCKETBASE_URL=http://127.0.0.1:8080 - API_KEY=test - POCKETBASE_ADMIN_EMAIL=admin@gmail.com - POCKETBASE_ADMIN_PASSWORD=password - TITLE=Markopolis - CAP1=Markdown - CAP2="Self-hosting" - CAP3="Knowledge Garden" volumes: - ./pb_data:/app/pb ================================================ FILE: docs/Changelog/3.0.0.md ================================================ --- title: Version 3.0.0 date: 09-24-2024 tags: - 3.0.0 --- ## Backend Rewrite * **Technology Shift:** The backend has been completely rewritten, moving from Python to SwellKit. * **Database:** Transitioned from file-based management to using PocketBase for the backend database. This version is a full rewrite of the backend in sveltekit. The choice was made as managing both the frontend and backend using the same language and framework simplifies the architecture and management a lot. Additionally pocketbase provides a nice `js` interface which is also nice. ## UI Enhancements - The UI has been refined using ShadeCN, and Tailwind CSS. ## New Features * The new version introduces support for using relative paths in wiki links and images. * Tag Management * Tag Pages: Tags now get their own dedicated pages. * Menu Bar Icon: A new menu bar icon has been added to view the list of all tags. * Simplified Python CLI Tool * The Python CLI tool has been simplified to remove most dependencies. ================================================ FILE: docs/Development/APIs.md ================================================ --- title: API overview date: 22-09-2024 tags: - api - backend publish: true --- This section details the core operations of the app, specifically the backend. The backend exposes various REST API endpoints which can serve different types of requests related to markdown files. Below we detail each of them. ## Overview This document provides an overview of the API endpoints for the Marco Polo's application. The application is built using SvelteKit for the backend API and PocketBase for the database. A Python package is also used to expose a CLI for uploading files to the server. ## Endpoints ### Upload API > [!important] > **Endpoint:** ==/api/upload== > **Method:** *POST* - **Description** Uploads markdown files to the server, sets up the database, parses HTML, and stores the compiled results and original files in the database. - **Parameters** - `file`: The markdown file to be uploaded. - `url`: Absolute path of file from root of vault. #### Details Refer [[Markdown Rendering]] --- ### LS API - **Endpoint** `/api/ls` - **Method** GET - **Description** Builds a file tree from the URL field in each record of the MDBase collection. - **Responses**: - `200 OK`: Returns a JSON response with the file tree. - `500 Internal Server Error`: Error in processing the request. --- ### Search API - **Endpoint** `/api/search` - **Method** GET - **Description** Performs a fuzzy search through the content stored in the database. - **Parameters**: - `query`: The search query string. - **Responses**: - `200 OK`: Returns search results with snippets containing matches. - `404 Not Found`: No matches found. --- ### Links API - **Endpoint** `/api/links` - **Method** GET - **Description** Retrieves all links and backlinks for a given markdown file. - **Parameters** - `url`: The URL of the markdown file. - **Responses** - `200 OK`: Returns forward and backward links. - `404 Not Found`: File not found. --- ### Backlinks API - **Endpoint** `/api/backlinks` - **Method** GET - **Description** Retrieves only the backlinks for a given markdown file. - **Parameters** - `url`: The URL of the markdown file. - **Responses**: - `200 OK`: Returns backlinks. - `404 Not Found`: File not found. --- ### Image API - **Endpoint** `/api/image` - **Method** GET - **Description**: Fetches images from the database. - **Parameters** - `url`: The URL of the image file. - **Responses**: - `200 OK`: Returns the image file. - `404 Not Found`: Image not found. ================================================ FILE: docs/Development/Markdown Rendering.md ================================================ --- title: Rendering Markdown as HTML date: 09-22-2024 tags: - markdown - mdsvex publish: true --- This document provides an in-depth explanation of the markdown parsing process used in the Marco Polo's application. The parsing is implemented using the `md-swex` library, which allows for the integration of Svelte components within markdown files. ## Parsing Process 1. **Markdown to HTML Conversion**: - The `md-swex` library is used to parse markdown files and convert them into HTML blocks. - The `compile` function of `md-swex` is utilized to directly compile markdown content. 2. **Database Storage**: - Parsed content is stored in the PocketBase database. - Fields include ID, title, parsed HTML content, URL, markdown file, front matter (as JSON), and relational fields for backlinks and forward links. 3. **Plugins and Extensions**: - **Remark Plugins**: Used for processing markdown abstract syntax trees. - `Remark Math`: Renders LaTeX equations. - `Remark Footnotes`: Processes footnotes. - `Custom Wikilink Plugin`: Resolves relative links. - `Custom Obsidian Image Plugin`: Handles inline images. - `Custom Remark Mermaid Plugin`: Processes Mermaid diagrams. 4. **Custom Wikilink Plugin**: - Recognizes Wikilink syntax and evaluates relative paths. - Uses front matter to access the file path and resolve links. 5. **Handling Code Blocks**: - Addresses issues with `md-swex` parsing code blocks as inline HTML. - Cleanup functions remove unwanted syntax to ensure proper rendering. 6. **Operational Checks**: - Ensures the existence of necessary database collections (e.g., `mdbase`, `attachments`). - Handles file uploads and updates, storing markdown and image files appropriately. ## Conclusion The markdown parsing component is integral to the Marco Polo's application, enabling efficient storage and retrieval of markdown content. The use of `md-swex` and various plugins ensures robust parsing and rendering capabilities. ================================================ FILE: docs/Development/components.md ================================================ --- title: Components behind Markopolis tags: - development - working date: 09/20/2024 publish: true --- Markopolis is built using 2 frameworks: [sveltekit](https://svelte.dev) and [pocketbase](https://pocketbase.docs). Pocketbase is used for database and the backend API is implemented using sveltekit. It additionally has a client side convenience python CLI interface. The sveltekit webapp exposes APIs for interaction, pre-renders and routes the websites. The functionality of the app begins with the python CLI interface. Running the sync command uploads files using an API. The API on recieving the file, converts it to html and stores it in the database along with the original file. The sveltekit app then uses dynamic routing to search and fetch the file from the database and renders it as a webpage. On the high-level Markopolis consists of a backend and a database. The front-end interacts with the backend and requests things needed to render pages. The backend, according to the requests, pulls data from the database and returns them. The front-end interacts with the backend via REST APIs exposed by the backend and for webpages also uses Server Side Rendering(SSR). Below is a diagram illustrating the data and control flow: ```mermaid sequenceDiagram Frontend->>Backend: API calls Backend-->>Database: fetch data using PocketBase API Database-->>Backend: Requested data Backend-)Frontend: Requested data ``` ## Backend The backends primary function is ================================================ FILE: docs/Markdown Syntax.md ================================================ --- publish: true tags: - syntax - markdown title: Markdown Syntax --- # Headings ```markdown # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ``` # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ## Horizontal line --- ## Tags ```markdown #tag1 #tag2 #tag3 ``` #tag1 #tag2 #tag3 ## Images ### embed images Image names should be unique. Duplicate images will be overwritten. ```markdown ![[image.png]] ![](image.png) ``` ![[image.png]] ![](image.png) ### external images ```markdown ![Engelbart](https://history-computer.com/ModernComputer/Basis/images/Engelbart.jpg) ``` ![Engelbart](https://history-computer.com/ModernComputer/Basis/images/Engelbart.jpg) ## Wikilinks ```markdown [[Installation]] [[f1/test]] [[f2/test]] ``` [[installation]] [[f1/test]] [[f2/test]] ## Text formatting ```markdown **Bold text** *Italic text* ~~this puts a strikethrough~~ ==this highlights text== **Bold text and _nested italic_ text** ***Bold and italic text*** ``` **Bold text** *Italic text* ~~this puts a strikethrough~~ ==this highlights text== **Bold text and _nested italic_ text** ***Bold and italic text*** ## Equations ```markdown $$ \sum_i = x $$ ``` $$ \sum_i = x $$ ## Footnotes ```markdown This is a simple footnote[^1]. [^1]: This is the referenced text. [^2]: Add 2 spaces at the start of each new line. This lets you write footnotes that span multiple lines. [^note]: Named footnotes still appear as numbers, but can make it easier to identify and link references. ``` This is a simple footnote[^1]. ## Quotes ```markdown > Human beings face ever more complex and urgent problems, and their effectiveness in dealing with these problems is a matter that is critical to the stability and continued progress of society. \- Doug Engelbart, 1961 ``` > Human beings face ever more complex and urgent problems, and their effectiveness in dealing with these problems is a matter that is critical to the stability and continued progress of society. \- Doug Engelbart, 1961 ## Tables ```markdown | First name | Last name | | ---------- | --------- | | Max | Planck | | Marie | Curie | ``` | First name | Last name | | ---------- | --------- | | Max | Planck | | Marie | Curie | The vertical bars on either side of the table are optional. Cells don't need to be perfectly aligned with the columns. Each header row must have at least two hyphens. ```markdown First name | Last name -- | -- Max | Planck Marie | Curie ``` First name | Last name -- | -- Max | Planck Marie | Curie ## Mermaid diagrams Some text. ```mermaid graph TB A --> B B --> C ``` ```mermaid flowchart LR A[Osaka 7-8] --> B[Tokyo 9-11] B -.Nagano .-> C[Matsumoto] C -.Nagano & Toyama.-> D[Takayama 12] <--> D1(Hida no Sato village) B -.Nagano & Toyama.-> D C <-.bus.-> D D --Toyama---> E <--> D2([Onsen 14]) --> F E[Kanazawa 13] ---> F[Kyoto 15-18] <--> F2(Uji) <--> F1(Nara) F <-.-> F4(Himeji) ``` ### Large chart ```mermaid timeline section .NET Framework 2000 - 2005 : .NET Framework 1.0 : .NET Framework 1.0 SP1 : .NET Framework 1.0 SP2 : .NET Framework 1.1 : .NET Framework 1.0 SP3 : .NET Framework 2.0 2006 - 2009 : .NET Framework 3.0 : .NET Framework 3.5 : .NET Framework 2.0 SP 1 : .NET Framework 3.0 SP 1 : .NET Framework 2.0 SP 2 : .NET Framework 3.0 SP 2 : .NET Framework 3.5 SP 1 2010 - 2015 : .NET Framework 4.0 : .NET Framework 4.5 : .NET Framework 4.5.1 : .NET Framework 4.5.2 : .NET Framework 4.6 : .NET Framework 4.6.1 section .NET Core 2016 - 2017 : .NET Core 1.0 : .NET Core 1.1 : .NET Framework 4.6.2 : .NET Core 2.0 : .NET Framework 4.7 : .NET Framework 4.7.1 2018 - 2019 : .NET Core 2.1 : .NET Core 2.2 : .NET Framework 4.7.2 : .NET Core 3.0 : .NET Core 3.1 : .NET Framework 4.8 section Modern .NET 2020 : .NET 5 2021 : .NET 6 2022 : .NET 7 : .NET Framework 4.8.1 ``` ## Callouts > [!abstract] > Lorem ipsum dolor sit amet > [!info] > Lorem ipsum dolor sit amet > [!todo] > Lorem ipsum dolor sit amet > [!tip] > Lorem ipsum dolor sit amet > [!success] > Lorem ipsum dolor sit amet > [!question] > Lorem ipsum dolor sit amet > [!warning] > Lorem ipsum dolor sit amet > [!failure] > Lorem ipsum dolor sit amet > [!danger] > Lorem ipsum dolor sit amet > [!bug] > Lorem ipsum dolor sit amet > [!example] > Lorem ipsum dolor sit amet > [!quote] > Lorem ipsum dolor sit amet > [!tip] Title-only callout [^1]: This is the referenced text. [^2]: Add 2 spaces at the start of each new line. This lets you write footnotes that span multiple lines. [^note]: Named footnotes still appear as numbers, but can make it easier to identify and link references. ================================================ FILE: docs/installation.md ================================================ --- title: Installation date: 09-24-2024 tags: - install - docker --- Installing Markopolis involves two steps. First deploying the server. Second installing the CLI tool. The CLI tool provides a utility command to upload your markdown files to the server. The articles are published as soon as this command is run. ## Server installation We will be using Docker for deploying Markopolis. Create a docker-compose and configuring environment variables. Make sure to generate and add a secure `API_KEY`. Allocate persistent storage for the Markdown files. First create a `.env` file to configure the following environment variables. ```bash POCKETBASE_URL=http://127.0.0.1:8090 API_KEY= Self-hostable Obsidian Publish ## Why Markdown files are my preferred choice for storing information. It's simple and is future proof. Having used Obsidian and liking it a lot, I moved back to using my text editor as Obsidian was too distracting. The customizability is endless and I found myself frequently caught down rabbit holes, trying to optimize for the perfect setup. I had to end the insanity. I have been using Markdown-Oxide along with my editor and that keeps it super simple. However, I miss some of the features offered by Obsidian via plugins like 1-click Publish, auto-tagging, notes discovery, etc. I decided to build something that would help me to easily publish my notes online, and can be self-hosted on my own hardware. This got me thinking about using REST APIs as an interface to work with markdown files. That way, I can implement my own features around my notes. Hence, Markopolis. ## Features - **Easy setup** Extremely simple to deploy and use - **Easy publish** Publish notes online with a single command - **Markdown API interface** Interact with aspecs of markdown using REST APIs - **Extensible** Extendable using exposed APIs - **Instand rendering** Article is available online as soon as ypu publish - **Full text search** Fuzzy search across your entire notes vault - **Obsidian markdown flavor** Maintains compatibility with obsidian markdown syntax. Supports callouts, equations, code highlighting etc. - **Dark & Light modes** Supports toggling between light and dark themes - **Easy maintenance** Requires very little to no maintenance - **Docker support** Available as docker images to self host ## Demo This website is hosted using Markopolis and is a live demo. These notes are used to demonstrate the various aspects of Markopolis. Checkout the [[Markdown Syntax]] page for a full showcase of all supported markdown syntax. ## About me Hi, I'm [Rishi](https://rishikanth.me), a recent PhD graduate and soon to start as an Applied Researcher. I'm an avid self-hoster and a strong proponent of open-source software. I'm based out of Washington and enjoy solving practical problems with code. ================================================ FILE: docs/roadmap.md ================================================ --- title: Development Roadmap date: 09-23-2024 publish: true --- This page lists a bunch of features and improvements that I plan to take on. These are based on my own needs, but I'm more than happy to take feature requests. If you face an issue or want a particular feature feel free to open a Github issue and I'll address it. ## UX & UI improvements - **Better blockquote styling** Fix the odd alignment of quotes and text - **Nested checkbox lists** Handle formatting of nested to-do / checkbox lists - **Upload only changed files** Currently overwriting all files irrespective ## Features - **Password protection** Hide specific pages behind a password - **Selective AI chat** Chat with selected notes using OpenAI / Claude - **Graph view** Visualize the backlinks network as a 3D graph - **Graph Navigation** navigate notes via graphs - **Graph interactions** Interact with your notes using 3D graphs - advanced filtering - AI chat with sub-graph as context - **Estimated reading time** - **ToDo management** Show, manage and edit ToDos - **Daily notes** Render daily notes under a personal login - **Auto tagging** Auto tagging using graph community detection ## Bug fixes - **Cleanup dangling tags** Delete unused tags left behind by file deletion - **Highlight.js** Something weird is going and syntax highlighting doesn't work as intended ================================================ FILE: docs/usage.md ================================================ --- title: Usage date: 09-24-2024 --- Initially the app starts with an empty database as there are no notes. So we will begin by uploading some notes. ### Uploading files to server Open a terminal and `cd` to the root directory of your notes vault. This is the directory of your notes. Then run `mdsync` in the vault directory. The command will scan for all markdown and image files. > [!note] > You can delete files on the server by simply deleting the file locally in your > vault and then running `mdsync` command. ### WikiLinks and Images Markopolis supports Obsidian style WikiLinks and Images. Previously Markopolis only supported absolute path from note vault. However, from 3.0.0 Markopolis now supports relative path as well. > [!Tip] > Checkout [[Markdown Syntax]] to see how different markdown syntax renders. ================================================ FILE: eslint.config.js ================================================ import js from '@eslint/js'; import ts from 'typescript-eslint'; import svelte from 'eslint-plugin-svelte'; import prettier from 'eslint-config-prettier'; import globals from 'globals'; /** @type {import('eslint').Linter.Config[]} */ export default [ js.configs.recommended, ...ts.configs.recommended, ...svelte.configs['flat/recommended'], prettier, ...svelte.configs['flat/prettier'], { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, { files: ['**/*.svelte'], languageOptions: { parserOptions: { parser: ts.parser } } }, { ignores: ['build/', '.svelte-kit/', 'dist/'] } ]; ================================================ FILE: package.json ================================================ { "name": "godamn", "version": "0.0.1", "private": true, "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . && eslint .", "format": "prettier --write ." }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.2.4", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tailwindcss/typography": "^0.5.14", "@types/eslint": "^9.6.0", "autoprefixer": "^10.4.20", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", "globals": "^15.0.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "prettier-plugin-tailwindcss": "^0.6.5", "remark-footnotes": "2.0", "remark-math": "^3.0.0", "svelte": "^4.2.7", "svelte-check": "^4.0.0", "tailwindcss": "^3.4.9", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", "vite": "^5.0.3" }, "type": "module", "dependencies": { "@leeoniya/ufuzzy": "^1.0.14", "@pondorasti/remark-img-links": "^1.0.8", "@threlte/core": "^7.3.1", "@threlte/extras": "^8.11.5", "3d-force-graph": "^1.73.3", "bits-ui": "^0.21.15", "clsx": "^2.1.1", "d3-force": "^3.0.0", "d3-force-3d": "^3.0.5", "formsnap": "^1.0.1", "highlight.js": "^11.10.0", "js-yaml": "^4.1.0", "katex": "^0.16.11", "lodash-es": "^4.17.21", "lucide-svelte": "^0.441.0", "marked": "^14.1.3", "marked-admonition-extension": "^0.0.4", "marked-alert": "^2.1.0", "mdsvex": "^0.12.3", "mdsvex-relative-images": "^1.0.3", "mermaid": "^11.2.1", "mode-watcher": "^0.4.1", "pocketbase": "^0.21.5", "rehype-autolink-headings": "^7.1.0", "rehype-callouts": "^1.0.3", "rehype-highlight": "^7.0.0", "rehype-katex": "^7.0.1", "rehype-katex-svelte": "^1.2.0", "rehype-mermaid": "^2.1.0", "remark-gfm": "^4.0.0", "remark-wiki-link": "^0.0.4", "svelte-markdown": "^0.4.1", "svelte-radix": "^1.1.1", "sveltekit-superforms": "^2.19.0", "tailwind-merge": "^2.5.2", "tailwind-variants": "^0.2.1", "threlte": "^3.13.1", "unist-util-visit": "^5.0.0", "zod": "^3.23.8" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {} } }; ================================================ FILE: src/app.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; --destructive: 0 72.2% 50.6%; --destructive-foreground: 0 0% 98%; --ring: 0 0% 3.9%; --radius: 0.5rem; } .dark { /* --background: 0 0% 3.9%; */ /* --foreground: 0 0% 98%; */ --background: 0 0% 9%; --foreground: 30 0% 96%; /* --foreground: 30 0% 78%; */ /* --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%; */ --muted: 0 0% 15%; --muted-foreground: 30 0% 78%; --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; --border: 0 0% 14.9%; --input: 0 0% 14.9%; --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; --ring: 0 0% 83.1%; } h1 { @apply text-6xl; @apply mb-4 mt-10; } h2 { @apply text-4xl; @apply mb-3 mt-8; } h3 { @apply text-2xl; @apply mb-2 mt-6; } h4 { @apply text-xl; @apply mb-1 mt-4; } p { @apply my-2; } a { @apply text-[#0f62fe] dark:text-[#78a9ff]; } a:hover { @apply text-[#0043ce] dark:text-[#a6c8ff]; } * { @apply border-border; } body { @apply bg-background text-foreground; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Bold.ttf') format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-BoldItalic.ttf') format('truetype'); font-weight: bold; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ExtraLight.ttf') format('truetype'); font-weight: 200; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ExtraLightItalic.ttf') format('truetype'); font-weight: 200; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Italic.ttf') format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Light.ttf') format('truetype'); font-weight: 300; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-LightItalic.ttf') format('truetype'); font-weight: 300; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Medium.ttf') format('truetype'); font-weight: 500; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-MediumItalic.ttf') format('truetype'); font-weight: 500; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-SemiBold.ttf') format('truetype'); font-weight: 600; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-SemiBoldItalic.ttf') format('truetype'); font-weight: 600; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Text.ttf') format('truetype'); font-weight: 400; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-TextItalic.ttf') format('truetype'); font-weight: 400; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Thin.ttf') format('truetype'); font-weight: 100; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ThinItalic.ttf') format('truetype'); font-weight: 100; font-style: italic; } @font-face { font-family: 'Megrim'; src: url('/fonts/Megrim-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } /* ---- IBM Plex Mono ---- */ @font-face { font-family: 'IBM Plex Mono'; font-weight: 700; font-style: normal; src: url('/fonts/IBMPlexMono-Bold.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 700; font-style: italic; src: url('/fonts/IBMPlexMono-BoldItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 200; font-style: normal; src: url('/fonts/IBMPlexMono-ExtraLight.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 200; font-style: italic; src: url('/fonts/IBMPlexMono-ExtraLightItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: italic; src: url('/fonts/IBMPlexMono-Italic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 300; font-style: normal; src: url('/fonts/IBMPlexMono-Light.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 300; font-style: italic; src: url('/fonts/IBMPlexMono-LightItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-style: normal; src: url('/fonts/IBMPlexMono-Medium.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-style: italic; src: url('/fonts/IBMPlexMono-MediumItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; src: url('/fonts/IBMPlexMono-Regular.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: normal; src: url('/fonts/IBMPlexMono-SemiBold.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: italic; src: url('/fonts/IBMPlexMono-SemiBoldItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; src: url('/fonts/IBMPlexMono-Text.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: italic; src: url('/fonts/IBMPlexMono-TextItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 100; font-style: normal; src: url('/fonts/IBMPlexMono-Thin.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 100; font-style: italic; src: url('/fonts/IBMPlexMono-ThinItalic.ttf') format('truetype'); } /* Lombok */ @font-face { font-family: 'Lombok'; font-weight: 400; font-style: normal; src: url('/fonts/Lombok.otf') format('opentype'); } } ================================================ FILE: src/app.d.ts ================================================ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { namespace App { interface Locals { pb: import('pocketbase').default; } // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} } } export {}; ================================================ FILE: src/app.html ================================================ %sveltekit.head%
%sveltekit.body%
================================================ FILE: src/lib/components/FileTree.svelte ================================================ {#if node}
e.key === "Enter" && toggleExpand()} role="button" tabindex="0" > {#if isFolder} {#if isExpanded} {:else} {/if} {:else} {/if} {#if isFolder} {node.name} {:else if node.url} {node.title} {:else} {node.title} {/if}
{#if isFolder && isExpanded}
{#each node.children as childNode} {/each}
{/if}
{:else}
{#each fileTree as rootNode} {/each}
{/if} ================================================ FILE: src/lib/components/GraphScene.svelte ================================================ { ref.lookAt(0, 0, 0); }} > scale.set(1.2)} on:pointerleave={() => scale.set(1)}> {#each nodes as node} {/each} {#each edges as edge} {/each} ================================================ FILE: src/lib/components/LoginForm.svelte ================================================ Login
Username This is your public display name. Password This is your public display name. Submit
================================================ FILE: src/lib/components/MDGraph.svelte ================================================
{#if browser}
{/if}
================================================ FILE: src/lib/components/MDsvexRenderer.svelte ================================================
================================================ FILE: src/lib/components/MarkdownGraph.svelte ================================================
================================================ FILE: src/lib/components/Scene.svelte ================================================ { ref.lookAt(0, 0, 0); }} > scale.set(1.5)} on:pointerleave={() => scale.set(1)}> ================================================ FILE: src/lib/components/SearchComponent.svelte ================================================ Search
{#if isLoading} {:else if searchQuery} {:else} {/if}
{#if searchResults.length > 0}
{#each searchResults as result, index}
{/each}
{:else if searchQuery && !isLoading}

No results found

{/if}
================================================ FILE: src/lib/components/Sidebar.svelte ================================================
{title}
{captions[0]} / {captions[1]}
{captions[2]}
================================================ FILE: src/lib/components/TagBar.svelte ================================================
TAGS
{#each tags as tag (tag.name)} {/each}
================================================ FILE: src/lib/components/TopBar.svelte ================================================
{#if title}
{title}
{/if}
================================================ FILE: src/lib/components/award.svelte ================================================ ================================================ FILE: src/lib/components/schema.ts ================================================ import { z } from 'zod'; export const formSchema = z.object({ username: z.string(), password: z.string() }); export type FormSchema = typeof formSchema; ================================================ FILE: src/lib/components/ui/button/button.svelte ================================================ ================================================ FILE: src/lib/components/ui/button/index.ts ================================================ import type { Button as ButtonPrimitive } from "bits-ui"; import { type VariantProps, tv } from "tailwind-variants"; import Root from "./button.svelte"; const buttonVariants = tv({ base: "focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50", variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm", outline: "border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", }, }, defaultVariants: { variant: "default", size: "default", }, }); type Variant = VariantProps["variant"]; type Size = VariantProps["size"]; type Props = ButtonPrimitive.Props & { variant?: Variant; size?: Size; }; type Events = ButtonPrimitive.Events; export { Root, type Props, type Events, // Root as Button, type Props as ButtonProps, type Events as ButtonEvents, buttonVariants, }; ================================================ FILE: src/lib/components/ui/card/card-content.svelte ================================================
================================================ FILE: src/lib/components/ui/card/card-description.svelte ================================================

================================================ FILE: src/lib/components/ui/card/card-footer.svelte ================================================
================================================ FILE: src/lib/components/ui/card/card-header.svelte ================================================
================================================ FILE: src/lib/components/ui/card/card-title.svelte ================================================ ================================================ FILE: src/lib/components/ui/card/card.svelte ================================================
================================================ FILE: src/lib/components/ui/card/index.ts ================================================ import Root from "./card.svelte"; import Content from "./card-content.svelte"; import Description from "./card-description.svelte"; import Footer from "./card-footer.svelte"; import Header from "./card-header.svelte"; import Title from "./card-title.svelte"; export { Root, Content, Description, Footer, Header, Title, // Root as Card, Content as CardContent, Description as CardDescription, Footer as CardFooter, Header as CardHeader, Title as CardTitle, }; export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; ================================================ FILE: src/lib/components/ui/dialog/dialog-content.svelte ================================================ Close ================================================ FILE: src/lib/components/ui/dialog/dialog-description.svelte ================================================ ================================================ FILE: src/lib/components/ui/dialog/dialog-footer.svelte ================================================
================================================ FILE: src/lib/components/ui/dialog/dialog-header.svelte ================================================
================================================ FILE: src/lib/components/ui/dialog/dialog-overlay.svelte ================================================ ================================================ FILE: src/lib/components/ui/dialog/dialog-portal.svelte ================================================ ================================================ FILE: src/lib/components/ui/dialog/dialog-title.svelte ================================================ ================================================ FILE: src/lib/components/ui/dialog/index.ts ================================================ import { Dialog as DialogPrimitive } from "bits-ui"; import Title from "./dialog-title.svelte"; import Portal from "./dialog-portal.svelte"; import Footer from "./dialog-footer.svelte"; import Header from "./dialog-header.svelte"; import Overlay from "./dialog-overlay.svelte"; import Content from "./dialog-content.svelte"; import Description from "./dialog-description.svelte"; const Root = DialogPrimitive.Root; const Trigger = DialogPrimitive.Trigger; const Close = DialogPrimitive.Close; export { Root, Title, Portal, Footer, Header, Trigger, Overlay, Content, Description, Close, // Root as Dialog, Title as DialogTitle, Portal as DialogPortal, Footer as DialogFooter, Header as DialogHeader, Trigger as DialogTrigger, Overlay as DialogOverlay, Content as DialogContent, Description as DialogDescription, Close as DialogClose, }; ================================================ FILE: src/lib/components/ui/form/form-button.svelte ================================================ ================================================ FILE: src/lib/components/ui/form/form-description.svelte ================================================ ================================================ FILE: src/lib/components/ui/form/form-element-field.svelte ================================================
================================================ FILE: src/lib/components/ui/form/form-field-errors.svelte ================================================ {#each errors as error}
{error}
{/each}
================================================ FILE: src/lib/components/ui/form/form-field.svelte ================================================
================================================ FILE: src/lib/components/ui/form/form-fieldset.svelte ================================================ ================================================ FILE: src/lib/components/ui/form/form-label.svelte ================================================ ================================================ FILE: src/lib/components/ui/form/form-legend.svelte ================================================ ================================================ FILE: src/lib/components/ui/form/index.ts ================================================ import * as FormPrimitive from "formsnap"; import Description from "./form-description.svelte"; import Label from "./form-label.svelte"; import FieldErrors from "./form-field-errors.svelte"; import Field from "./form-field.svelte"; import Button from "./form-button.svelte"; import Fieldset from "./form-fieldset.svelte"; import Legend from "./form-legend.svelte"; import ElementField from "./form-element-field.svelte"; const Control = FormPrimitive.Control; export { Field, Control, Label, FieldErrors, Description, Fieldset, Legend, ElementField, Button, // Field as FormField, Control as FormControl, Description as FormDescription, Label as FormLabel, FieldErrors as FormFieldErrors, Fieldset as FormFieldset, Legend as FormLegend, ElementField as FormElementField, Button as FormButton, }; ================================================ FILE: src/lib/components/ui/input/index.ts ================================================ import Root from "./input.svelte"; export type FormInputEvent = T & { currentTarget: EventTarget & HTMLInputElement; }; export type InputEvents = { blur: FormInputEvent; change: FormInputEvent; click: FormInputEvent; focus: FormInputEvent; focusin: FormInputEvent; focusout: FormInputEvent; keydown: FormInputEvent; keypress: FormInputEvent; keyup: FormInputEvent; mouseover: FormInputEvent; mouseenter: FormInputEvent; mouseleave: FormInputEvent; mousemove: FormInputEvent; paste: FormInputEvent; input: FormInputEvent; wheel: FormInputEvent; }; export { Root, // Root as Input, }; ================================================ FILE: src/lib/components/ui/input/input.svelte ================================================ ================================================ FILE: src/lib/components/ui/label/index.ts ================================================ import Root from "./label.svelte"; export { Root, // Root as Label, }; ================================================ FILE: src/lib/components/ui/label/label.svelte ================================================ ================================================ FILE: src/lib/components/ui/scroll-area/index.ts ================================================ import Scrollbar from "./scroll-area-scrollbar.svelte"; import Root from "./scroll-area.svelte"; export { Root, Scrollbar, //, Root as ScrollArea, Scrollbar as ScrollAreaScrollbar, }; ================================================ FILE: src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte ================================================ ================================================ FILE: src/lib/components/ui/scroll-area/scroll-area.svelte ================================================ {#if orientation === "vertical" || orientation === "both"} {/if} {#if orientation === "horizontal" || orientation === "both"} {/if} ================================================ FILE: src/lib/components/ui/separator/index.ts ================================================ import Root from "./separator.svelte"; export { Root, // Root as Separator, }; ================================================ FILE: src/lib/components/ui/separator/separator.svelte ================================================ ================================================ FILE: src/lib/highlightCode.ts ================================================ import { writable } from 'svelte/store'; import { mode } from 'mode-watcher'; function createHighlightStore() { const { subscribe, set } = writable('github'); return { subscribe, setTheme: (isDark: boolean) => { set(isDark ? 'github-dark' : 'github'); } }; } export const highlightTheme = createHighlightStore(); ================================================ FILE: src/lib/index.ts ================================================ // place files you want to import through the `$lib` alias in this folder. ================================================ FILE: src/lib/md.ts ================================================ // src/lib/md.ts import yaml from 'js-yaml'; /** * Function to add or update frontmatter in Markdown content. * * @param fileContent - The content of the Markdown file as a string. * @param url - The URL to be added to the frontmatter. * @returns The modified Markdown content with updated frontmatter. */ export function addFrontmatterToMarkdown(fileContent: string, url: string): string { // Regular expression to detect existing YAML frontmatter const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/; const match = fileContent.match(frontmatterRegex); let newContent: string; if (match) { // YAML frontmatter exists, parse the existing frontmatter const existingFrontmatter = yaml.load(match[1]) as Record || {}; // Add or update the 'url' field in the frontmatter existingFrontmatter['mdpath'] = url; // Convert the updated frontmatter back to YAML format const updatedFrontmatter = yaml.dump(existingFrontmatter); // Replace the old frontmatter with the updated one newContent = fileContent.replace(frontmatterRegex, `---\n${updatedFrontmatter}---\n`); } else { // No frontmatter exists, create new frontmatter const newFrontmatter = yaml.dump({ url }); // Prepend the new frontmatter to the file content newContent = `---\n${newFrontmatter}---\n${fileContent}`; } // Return the updated content return newContent; } ================================================ FILE: src/lib/pbStore.ts ================================================ import PocketBase from 'pocketbase'; import { writable } from 'svelte/store'; import { browser } from '$app/environment'; // Client-side PocketBase instance export const pb = new PocketBase('http://127.0.0.1:8090'); // Replace with your PocketBase URL export const currentUser = writable(pb.authStore.model); if (browser) { pb.authStore.onChange((auth) => { console.log('Client AuthStore changed', auth); currentUser.set(pb.authStore.model); }); } export async function login(email: string, password: string) { try { const authData = await pb.admins.authWithPassword(email, password); console.log('Logged in successfully', authData); return authData; } catch (error) { console.error('Login failed', error); throw error; } } export function logout() { pb.authStore.clear(); } ================================================ FILE: src/lib/pocketbase.ts ================================================ import PocketBase from 'pocketbase'; import { POCKETBASE_URL, POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD } from '$env/static/private'; let serverPb: PocketBase | null = null; export async function getAuthenticatedPocketBase() { if (!serverPb) { serverPb = new PocketBase(POCKETBASE_URL); serverPb.autoCancellation(false); } // Check if already authenticated and try refreshing the token if (serverPb.authStore.isValid) { try { await serverPb.collection('users').authRefresh(); console.log('Using existing server authentication'); return serverPb; } catch (error) { console.log('Server token refresh failed, re-authenticating'); } } // If not authenticated or refresh failed, login as admin try { await serverPb.admins.authWithPassword(POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD); console.log('New server authentication successful'); return serverPb; } catch (error) { console.error('Server authentication failed:', error); throw error; } } ================================================ FILE: src/lib/remark-plugins/footNotes.js ================================================ // src/lib/plugins/remark-footnote-html.js import { visit } from 'unist-util-visit'; function remarkFootnoteHTML() { return (tree) => { const footnotes = []; const footnoteMap = {}; // Collect footnote definitions visit(tree, 'footnoteDefinition', (node) => { const identifier = node.identifier; const content = node.children; footnoteMap[identifier] = content; console.log(node) }); // Replace footnote references with custom HTML visit(tree, 'footnoteReference', (node, index, parent) => { const identifier = node.identifier; const footnoteNumber = Object.keys(footnoteMap).indexOf(identifier) + 1; if (footnoteNumber === 0) return; const sup = { type: 'html', value: `${footnoteNumber}`, }; parent.children.splice(index, 1, sup); }); // Remove footnote definitions from the tree tree.children = tree.children.filter((node) => node.type !== 'footnoteDefinition'); // Append the footnotes section at the end const footnotesSection = { type: 'html', value: '
    ', }; tree.children.push(footnotesSection); Object.keys(footnoteMap).forEach((identifier, idx) => { const footnoteNumber = idx + 1; const content = footnoteMap[identifier] .map((child) => { if (child.type === 'text') { return child.value; } else { // Handle other node types as needed return ''; } }) .join(''); const footnoteItem = { type: 'html', value: `
  1. ${content} ↩︎
  2. `, }; tree.children.push(footnoteItem); }); // Close the ordered list and section tree.children.push({ type: 'html', value: '
', }); }; } export default remarkFootnoteHTML; ================================================ FILE: src/lib/remark-plugins/highlightSyn.js ================================================ import { visit } from 'unist-util-visit' function remarkHighlight() { return (tree) => { visit(tree, 'text', (node, index, parent) => { const matches = node.value.match(/==(.*?)==/g) if (!matches) return const children = [] let lastIndex = 0 matches.forEach((match) => { const startIndex = node.value.indexOf(match, lastIndex) const endIndex = startIndex + match.length // Add text before the highlight if (startIndex > lastIndex) { children.push({ type: 'text', value: node.value.slice(lastIndex, startIndex) }) } // Add the highlighted text with a span and class children.push({ type: 'span', data: { hName: 'span', hProperties: { className: ['highlight'] } }, children: [ { type: 'text', value: match.slice(2, -2) // Remove '==' from the start and end } ] }) lastIndex = endIndex }) // Add any remaining text after the last highlight if (lastIndex < node.value.length) { children.push({ type: 'text', value: node.value.slice(lastIndex) }) } // Replace the original node with the new children parent.children.splice(index, 1, ...children) }) } } export default remarkHighlight ================================================ FILE: src/lib/remark-plugins/imgRel.js ================================================ import { visit } from 'unist-util-visit'; import path from 'path'; export default function remarkLogImages() { return function transformer(tree, file) { if (!file || !file.data || !file.data.fm || !file.data.fm.mdpath) { throw new Error('File metadata with url is missing.'); } const url = file.data.fm.mdpath; // e.g., '/writing/f2/test' visit(tree, 'image', (node) => { // Extract the link part before any pipe (e.g., [[link|alias]]) const rawLink = node.url.trim(); // e.g., '../f1/test' console.log(node) if (!rawLink.includes('/api/img') && !rawLink.includes('://')) { const folder = path.dirname(url.split('.')[0]); const absPath = path.join(folder, rawLink); // e.g., 'mdpath/f1/test' console.log("============>", rawLink, absPath) node.url = `/api/img/${absPath}`; } }); } } ================================================ FILE: src/lib/remark-plugins/mermaidDiag.js ================================================ import { visit } from 'unist-util-visit'; // Create the plugin const remarkMermaid = () => { return (tree) => { visit(tree, 'code', (node) => { if (node.lang === 'mermaid') { // Replace the code block with a custom HTML structure node.type = 'html'; node.value = `
${node.value}
`; } }); }; }; export default remarkMermaid; ================================================ FILE: src/lib/remark-plugins/obsidianImage.js ================================================ import { visit } from 'unist-util-visit'; function obsidianImagePlugin() { return (tree) => { visit(tree, 'paragraph', (node) => { const newChildren = []; let i = 0; while (i < node.children.length) { const currentNode = node.children[i]; // Check if the current node is a 'text' node with a '!' if (currentNode.type === 'text' && currentNode.value === '!') { // Check if the next node is a 'wikiLink' node with an image file extension const nextNode = node.children[i + 1]; if ( nextNode && nextNode.type === 'wikiLink' && /\.(png|jpe?g|gif|svg|webp)$/.test(nextNode.value) ) { // Replace the '!' and 'wikiLink' with an 'image' node let newUrl = '/api/img/' + nextNode.value; newChildren.push({ type: 'image', url: newUrl, alt: nextNode.value.split('/').pop() // Use the filename as the alt text }); i += 2; // Skip both the 'text' and 'wikiLink' nodes continue; } } // If no match, just push the current node as-is newChildren.push(currentNode); i++; } // Replace the old children with the new set of children node.children = newChildren; }); }; } export default obsidianImagePlugin; ================================================ FILE: src/lib/remark-plugins/remarkTags.ts ================================================ import { visit } from 'unist-util-visit'; function remarkTags() { return (tree) => { visit(tree, 'text', (node, index, parent) => { const matches = node.value.match(/#[a-zA-Z0-9_-]+/g); if (!matches) return; const children = []; let lastIndex = 0; matches.forEach((match) => { const startIndex = node.value.indexOf(match, lastIndex); const endIndex = startIndex + match.length; // Add text before the tag if (startIndex > lastIndex) { children.push({ type: 'text', value: node.value.slice(lastIndex, startIndex) }); } // Add the tag with a span and class, including an anchor tag const tagName = match.slice(1); // Remove '#' from the start children.push({ type: 'span', data: { hName: 'span', hProperties: { className: ['tag'] } }, children: [ { type: 'element', data: { hName: 'a', hProperties: { href: `/tags/${tagName}`, className: ['tag-link'] } }, children: [ { type: 'text', value: tagName } ] } ] }); lastIndex = endIndex; }); // Add any remaining text after the last tag if (lastIndex < node.value.length) { children.push({ type: 'text', value: node.value.slice(lastIndex) }); } // Replace the original node with the new children parent.children.splice(index, 1, ...children); }); }; } export default remarkTags; ================================================ FILE: src/lib/server/auth.ts ================================================ import PocketBase from 'pocketbase'; import { POCKETBASE_URL, POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD } from '$env/static/private'; let pocketBaseInstance: PocketBase | null = null; export async function getAuthenticatedPocketBase() { if (!pocketBaseInstance) { pocketBaseInstance = new PocketBase(POCKETBASE_URL); pocketBaseInstance.autoCancellation(false); // Prevent cancellation of overlapping requests } // Check if the current authentication is valid if (pocketBaseInstance.authStore.isValid) { try { console.log('login valid'); // Check if logged in as a user (not admin) and refresh token if (pocketBaseInstance.authStore.model?.email !== POCKETBASE_ADMIN_EMAIL) { // Only refresh tokens for non-admin users await pocketBaseInstance.collection('users').authRefresh(); console.log('Token refreshed successfully'); } return pocketBaseInstance; } catch (error) { console.error('Token refresh failed:', error); } } // Login as admin if token is invalid or refresh failed try { console.log('Attempting to log in as admin...'); await pocketBaseInstance.admins.authWithPassword( POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD ); console.log('New admin authentication successful'); return pocketBaseInstance; } catch (error) { // Log any error encountered during login console.error('Admin login failed:', error); throw error; } } ================================================ FILE: src/lib/stores/sidebarStore.ts ================================================ import { writable } from 'svelte/store'; export const isSidebarVisible = writable(true); ================================================ FILE: src/lib/utils.ts ================================================ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { cubicOut } from "svelte/easing"; import type { TransitionConfig } from "svelte/transition"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } type FlyAndScaleParams = { y?: number; x?: number; start?: number; duration?: number; }; export const flyAndScale = ( node: Element, params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } ): TransitionConfig => { const style = getComputedStyle(node); const transform = style.transform === "none" ? "" : style.transform; const scaleConversion = ( valueA: number, scaleA: [number, number], scaleB: [number, number] ) => { const [minA, maxA] = scaleA; const [minB, maxB] = scaleB; const percentage = (valueA - minA) / (maxA - minA); const valueB = percentage * (maxB - minB) + minB; return valueB; }; const styleToString = ( style: Record ): string => { return Object.keys(style).reduce((str, key) => { if (style[key] === undefined) return str; return str + `${key}:${style[key]};`; }, ""); }; return { duration: params.duration ?? 200, delay: 0, css: (t) => { const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); return styleToString({ transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, opacity: t }); }, easing: cubicOut }; }; ================================================ FILE: src/routes/+layout.server.ts ================================================ import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { superValidate } from "sveltekit-superforms"; import { formSchema } from "$lib/components/schema"; import { zod } from "sveltekit-superforms/adapters"; import { TITLE, POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD, } from "$env/static/private"; import { CAP1, CAP2, CAP3 } from "$env/static/private"; export async function load({ fetch, params }) { const ftree = await fetch("/api/ls"); const tagresp = await fetch("/api/tags"); const tags = await tagresp.json(); const filetree = await ftree.json(); const siteTitle = TITLE; const captions = [CAP1, CAP2, CAP3]; console.log( "logged in with: ", POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD, CAP1, CAP2, ); return { filetree, siteTitle, tags, captions }; } ================================================ FILE: src/routes/+layout.svelte ================================================
{siteTitle}
{#if fileTree} {:else} Loading... {/if}
================================================ FILE: src/routes/+page.server.ts ================================================ import PocketBase from "pocketbase"; import { getAuthenticatedPocketBase } from "$lib/server/auth"; const pb = await getAuthenticatedPocketBase(); export async function load({ params }) { try { const mdbase = await pb.collections.getOne("mdbase"); const records = await pb.collection("mdbase").getList(1, 1, { filter: "frontmatter.home = true", sort: "-created", }); let post = null; if (records.items.length > 0) { post = records.items[0]; } if (post) { const backlinks = await getBacklinks(`${post.frontmatter.mdpath}`); const tags = post.expand?.tags.map((tag) => { return { name: tag.tag, }; }); return { post, title: post.title, backlinks, tags }; } else { return { post: null, title: "", backlinks: [], tags: [] }; } } catch (error) { console.error(`Failed to fetch post: ${error}`); return { message: `Failed to fetch post: ${error}` }; } } async function getBacklinks(url) { const mdbaseCollection = pb.collection("mdbase"); const documentUrl = url; try { if (!documentUrl) { return new Response( JSON.stringify({ message: "URL parameter is required" }), { status: 400, }, ); } const documents = await mdbaseCollection.getList(1, 1, { filter: `url="${documentUrl}"`, expand: "backlinks", }); if (documents.items.length === 0) { return new Response(JSON.stringify({ message: "Document not found" }), { status: 404, }); } const document = documents.items[0]; const backLinks = (document.expand?.backlinks || []).map((link) => ({ id: link.id, title: link.title, url: link.url, })); return backLinks; } catch (error: any) { console.error("Error in backlinks API:", error); return {}; } } ================================================ FILE: src/routes/+page.svelte ================================================ {#if data.post}
{data.title}
{#if data?.post?.frontmatter?.date}
{data.post.frontmatter.date.split(' ')[0]}
{/if} {#if tags}
{#each tags as tag (tag.name)} {tag.name} {/each}
{/if}

{#if data?.backlinks?.length > 0}
BACKLINKS
{/if} {#each data?.backlinks || [] as bl (bl.id)} {/each}
{:else}
Set homepage in the frontmatter of one of your markdown files to display it as home
{/if} ================================================ FILE: src/routes/[...post].md/+page.server.ts ================================================ import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import PocketBase from "pocketbase"; import { promises as fs } from "fs"; import { getAuthenticatedPocketBase } from "$lib/server/auth"; const pb = await getAuthenticatedPocketBase(); async function getBacklinks(url) { const mdbaseCollection = pb.collection("mdbase"); const documentUrl = url; try { if (!documentUrl) { return new Response( JSON.stringify({ message: "URL parameter is required" }), { status: 400, }, ); } const documents = await mdbaseCollection.getList(1, 1, { filter: `url="${documentUrl}"`, expand: "backlinks", }); if (documents.items.length === 0) { return new Response(JSON.stringify({ message: "Document not found" }), { status: 404, }); } const document = documents.items[0]; const backLinks = (document.expand?.backlinks || []).map((link) => ({ id: link.id, title: link.title, url: link.url, })); return backLinks; } catch (error: any) { console.error("Error in backlinks API:", error); return {}; } } async function computeGraphData(fileUrl) { const currentPage = await pb .collection("mdbase") .getFirstListItem(`url="${fileUrl}"`); const relatedPages = await pb.collection("mdbase").getList(1, 50, { filter: `id ?~ "${currentPage.backlinks}" || id ?~ "${currentPage.links}"`, }); // Use a Set to store unique node IDs const uniqueNodeIds = new Set([currentPage.id]); // Create nodes array with current page const nodes = [ { id: currentPage.id, label: currentPage.title, color: "#ff0000" }, ]; // Add related pages to nodes array, avoiding duplicates relatedPages.items.forEach((p) => { if (!uniqueNodeIds.has(p.id)) { uniqueNodeIds.add(p.id); nodes.push({ id: p.id, label: p.title, color: "#00ff00" }); } }); // Create edges array const edges = [ ...currentPage.links.map((link) => ({ from: currentPage.id, to: link })), ...currentPage.backlinks.map((backlink) => ({ from: backlink, to: currentPage.id, })), ]; return { nodes, edges }; } // Main load function export async function load({ params, fetch, locals }) { try { // Step 1: Authenticate /* console.log(pb); */ console.log(params.post); const post = await pb .collection("mdbase") .getFirstListItem(`url="${params.post}.md"`, { expand: "tags" }); const backlinks = await getBacklinks(`${params.post}.md`); // const graphData = await computeGraphData(`${params.post}.md`); const tags = post.expand?.tags.map((tag) => { return { name: tag.tag, }; }); console.log(tags); return { post, title: post.title, backlinks, tags }; } catch (error) { console.error(`Failed to fetch post: ${error}`); return { message: `Failed to fetch post: ${error}` }; } } ================================================ FILE: src/routes/[...post].md/+page.svelte ================================================
{data.title}
{#if data?.post?.frontmatter?.date}
{data.post.frontmatter.date.split(' ')[0]}
{/if} {#if tags}
{#each tags as tag (tag.name)} {tag.name} {/each}
{/if}

{#if data?.backlinks?.length > 0}
BACKLINKS
{/if} {#each data?.backlinks || [] as bl (bl.id)} {/each}
================================================ FILE: src/routes/about/+page.svelte ================================================ ================================================ FILE: src/routes/api/backlinks/+server.ts ================================================ import type { RequestHandler } from '@sveltejs/kit'; import PocketBase from 'pocketbase'; import { POCKETBASE_ADMIN_PASSWORD, POCKETBASE_ADMIN_EMAIL, POCKETBASE_URL, API_KEY } from '$env/static/private'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; const pb = await getAuthenticatedPocketBase(); export const GET: RequestHandler = async ({ url, request }) => { try { const mdbaseCollection = pb.collection('mdbase'); const documentUrl = url.searchParams.get('url'); if (!documentUrl) { return new Response(JSON.stringify({ message: 'URL parameter is required' }), { status: 400 }); } const documents = await mdbaseCollection.getList(1, 1, { filter: `url="${documentUrl}"`, expand: 'backlinks' }); if (documents.items.length === 0) { return new Response(JSON.stringify({ message: 'Document not found' }), { status: 404 }); } const document = documents.items[0]; const backLinks = (document.expand?.backlinks || []).map((link) => ({ id: link.id, title: link.title, url: link.url })); return new Response(JSON.stringify({ backLinks }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (error: any) { console.error('Error in backlinks API:', error); return new Response( JSON.stringify({ message: 'Failed to retrieve backlinks', error: error.message || 'Unknown error', details: error.data ? JSON.stringify(error.data) : 'No additional details' }), { status: 500 } ); } }; export const OPTIONS: RequestHandler = async () => { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-API-Key' } }); }; ================================================ FILE: src/routes/api/graph/+server.ts ================================================ import { json } from '@sveltejs/kit'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ url, request }) => { const fileUrl = url.searchParams.get('url'); console.log(fileUrl); if (!fileUrl) { return json({ error: 'Missing URL parameter' }, { status: 400 }); } try { const pb = await getAuthenticatedPocketBase(); const currentPage = await pb.collection('mdbase').getFirstListItem(`url="${fileUrl}"`); const graphData = await computeGraphData(currentPage); return json(graphData); } catch (error) { console.error('Error computing graph:', error); return json({ error: error }, { status: 500 }); } }; async function computeGraphData(currentPage) { const pb = await getAuthenticatedPocketBase(); const relatedPages = await pb.collection('mdbase').getList(1, 50, { filter: `id ?~ "${currentPage.backlinks}" || id ?~ "${currentPage.links}"` }); const nodes = [ { id: currentPage.id, label: currentPage.title, color: '#ff0000' }, // Current page (red) ...relatedPages.items.map((p) => ({ id: p.id, label: p.title, color: '#00ff00' })) // Related pages (green) ]; const edges = [ ...currentPage.links.map((link) => ({ from: currentPage.id, to: link })), ...currentPage.backlinks.map((backlink) => ({ from: backlink, to: currentPage.id })) ]; return { nodes, edges }; } ================================================ FILE: src/routes/api/hello/+server.ts ================================================ // src/routes/api/hello/+server.ts import type { RequestHandler } from './$types'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; export const GET: RequestHandler = async ({ request }) => { const pb = await getAuthenticatedPocketBase(); // Ensure the server is authenticated if (!pb.authStore.isValid) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }); } // Example of accessing PocketBase data const users = await pb.collection('users').getFullList(); return new Response(JSON.stringify({ data: users }), { status: 200 }); }; ================================================ FILE: src/routes/api/img/[...path]/+server.ts ================================================ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import PocketBase from 'pocketbase'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; export const GET: RequestHandler = async ({ params }) => { try { const pb = await getAuthenticatedPocketBase(); const imagePath = params.path; console.log('Requested image path:', imagePath); const record = await pb.collection('attachments').getFirstListItem(`url="${imagePath}"`); console.log('Found record:', record); if (!record) { throw error(404, 'Image not found'); } const fileUrl = pb.files.getUrl(record, record.file); console.log('File URL:', fileUrl); const fileResponse = await fetch(fileUrl); if (!fileResponse.ok) { throw error(500, 'Failed to fetch the image file'); } const contentType = fileResponse.headers.get('content-type') || getContentType(imagePath); console.log('Content Type:', contentType); // Get the filename from the record or use a default const filename = record.filename || 'image'; return new Response(fileResponse.body, { status: 200, headers: { 'Content-Type': contentType, 'Content-Disposition': `attachment; filename="${filename}"`, 'Cache-Control': 'public, max-age=3600' } }); } catch (err) { console.error('Error serving image:', err); throw error(500, 'Internal server error'); } }; function getContentType(filename: string): string { const ext = filename.split('.').pop()?.toLowerCase(); switch (ext) { case 'webp': return 'image/webp'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'png': return 'image/png'; case 'gif': return 'image/gif'; case 'svg': return 'image/svg+xml'; default: return 'application/octet-stream'; } } ================================================ FILE: src/routes/api/links/+server.ts ================================================ import type { RequestHandler } from '@sveltejs/kit'; import PocketBase from 'pocketbase'; import { POCKETBASE_ADMIN_PASSWORD, POCKETBASE_ADMIN_EMAIL, POCKETBASE_URL, API_KEY } from '$env/static/private'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; const pb = getAuthenticatedPocketBase(); export const GET: RequestHandler = async ({ url, request }) => { try { const mdbaseCollection = pb.collection('mdbase'); const documentUrl = url.searchParams.get('url'); if (!documentUrl) { return new Response(JSON.stringify({ message: 'URL parameter is required' }), { status: 400 }); } const documents = await mdbaseCollection.getList(1, 1, { filter: `url="${documentUrl}"`, expand: 'links,backlinks' }); if (documents.items.length === 0) { return new Response(JSON.stringify({ message: 'Document not found' }), { status: 404 }); } const document = documents.items[0]; const forwardLinks = (document.expand?.links || []).map((link) => ({ id: link.id, title: link.title, url: link.url })); const backLinks = (document.expand?.backlinks || []).map((link) => ({ id: link.id, title: link.title, url: link.url })); return new Response(JSON.stringify({ forwardLinks, backLinks }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (error: any) { console.error('Error in links API:', error); return new Response( JSON.stringify({ message: 'Failed to retrieve links', error: error.message || 'Unknown error', details: error.data ? JSON.stringify(error.data) : 'No additional details' }), { status: 500 } ); } }; export const OPTIONS: RequestHandler = async () => { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-API-Key' } }); }; ================================================ FILE: src/routes/api/ls/+server.ts ================================================ import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { getAuthenticatedPocketBase } from "$lib/server/auth"; interface FileNode { id: string; title: string; name: string; url: string; children: FileNode[]; } function buildFileTree(records: any[]): FileNode[] { const tree: FileNode[] = []; records.forEach((record) => { const pathParts = record.url.split("/").filter(Boolean); // Split the URL into parts let currentLevel = tree; pathParts.forEach((part: string, index: number) => { let existingNode = currentLevel.find((node) => node.name === part); // If the node doesn't exist, create a new one if (!existingNode) { existingNode = { id: "", // Only set if it's a file (at the last level) title: "", name: part, // The name of the folder or file url: "", // Only set if it's a file (at the last level) children: [], // This will hold the children (for folders) }; currentLevel.push(existingNode); } // If it's the last part of the path (a file), assign file properties if (index === pathParts.length - 1) { existingNode.id = record.id; existingNode.title = record.title; existingNode.url = record.url; } // Move to the next level in the tree currentLevel = existingNode.children; }); }); return tree; } export const GET: RequestHandler = async () => { try { const pb = await getAuthenticatedPocketBase(); const pageSize = 200; // Adjust this value based on your needs let page = 1; let allRecords: any[] = []; while (true) { const result = await pb.collection("mdbase").getList(page, pageSize, { sort: "url", }); allRecords = allRecords.concat(result.items); if (!result.items.length || result.items.length < pageSize) { break; } page++; } const fileTree = buildFileTree(allRecords); return json(fileTree); } catch (error) { console.error("Error fetching records:", error); return json({ error: "Failed to fetch file tree" }, { status: 500 }); } }; ================================================ FILE: src/routes/api/search/+server.ts ================================================ // api/search/+server.ts import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import uFuzzy from '@leeoniya/ufuzzy'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; function extractSnippet(content: string, query: string, snippetLength: number = 150) { const lowerContent = content.toLowerCase(); const lowerQuery = query.toLowerCase(); const index = lowerContent.indexOf(lowerQuery); if (index === -1) return content.slice(0, snippetLength); const start = Math.max(0, index - snippetLength / 2); const end = Math.min(content.length, index + query.length + snippetLength / 2); let snippet = content.slice(start, end); if (start > 0) snippet = '...' + snippet; if (end < content.length) snippet = snippet + '...'; return snippet; } export const GET: RequestHandler = async ({ url }) => { const query = url.searchParams.get('query'); if (!query) { return json({ error: 'Query parameter is required' }, { status: 400 }); } try { /* await authenticateAdmin(); */ const pb = await getAuthenticatedPocketBase(); const pbResults = await pb.collection('mdbase').getList(1, 1000, { fields: 'id,title,content,url' }); console.log('searched'); const haystack = pbResults.items.map((item) => item.title + ' ' + item.content); const uf = new uFuzzy(); let idxs = uf.filter(haystack, query); if (idxs != null && idxs.length > 0) { let info = uf.info(idxs, haystack, query); let order = uf.sort(info, haystack, query); const results = order.map((i) => { const item = pbResults.items[info.idx[i]]; return { title: item.title, url: `${item.url}`, snippet: extractSnippet(item.content, query) }; }); return json(results); } else { return json([]); } } catch (error) { console.error('Search error:', error); return json({ error: 'An error occurred during search' }, { status: 500 }); } }; ================================================ FILE: src/routes/api/tags/+server.ts ================================================ // src/routes/api/hello/+server.ts import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; export const GET: RequestHandler = async ({ request }) => { const pb = await getAuthenticatedPocketBase(); const records = await pb.collection('tags').getFullList(); const tags = records.map((tag) => { return { name: tag.tag }; }); return json(tags); }; ================================================ FILE: src/routes/api/upload/+server.ts ================================================ import type { RequestHandler } from "@sveltejs/kit"; import PocketBase from "pocketbase"; import path from "path"; import { compile } from "mdsvex"; import { getAuthenticatedPocketBase } from "$lib/server/auth"; import { addFrontmatterToMarkdown } from "$lib/md"; // Updated to accept fileContent and url import { visit } from "unist-util-visit"; import remarkFootnotes from "remark-footnotes"; import remarkTags from "$lib/remark-plugins/remarkTags"; import remarkHighlight from "$lib/remark-plugins/highlightSyn"; /* import rehypeMermaid from 'rehype-mermaid'; */ import remarkMermaid from "$lib/remark-plugins/mermaidDiag"; import remarkLogImages from "$lib/remark-plugins/imgRel"; /* import rehypeKatexSvelte from 'rehype-katex-svelte'; */ import rehypeKatex from "rehype-katex"; import remarkMath from "remark-math"; import rehypeCallouts from "rehype-callouts"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import wikiLink from "remark-wiki-link"; import obsidianImagePlugin from "$lib/remark-plugins/obsidianImage"; import rehypeWrapLiWithP from "$lib/rehype_plugins/wrapWithP"; import { POCKETBASE_ADMIN_PASSWORD, POCKETBASE_ADMIN_EMAIL, POCKETBASE_URL, API_KEY, // Add this line to import the API_KEY } from "$env/static/private"; function verifyApiKey(request: Request): boolean { const apiKey = request.headers.get("X-API-Key"); return apiKey === API_KEY; } /** * Middleware to check API key before processing the request */ async function apiKeyMiddleware( request: Request, handler: (req: Request) => Promise, ): Promise { if (!verifyApiKey(request)) { return new Response(JSON.stringify({ message: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" }, }); } return handler(request); } // Define interfaces for frontmatter and compiled markdown interface Frontmatter { title?: string; [key: string]: any; } interface CompiledMD { code: string; data?: { fm?: Frontmatter; }; } // Initialize PocketBase const pb = await getAuthenticatedPocketBase(); /** * Custom WikiLink transformer for Markdown processing. */ function customWikiLink() { return function transformer(tree: any, file: any) { // Ensure file metadata exists if (!file || !file.data || !file.data.fm || !file.data.fm.mdpath) { throw new Error("File metadata with url is missing."); } const url = file.data.fm.mdpath; // e.g., '/writing/f2/test' visit(tree, "wikiLink", (node: any) => { // Extract the link part before any pipe (e.g., [[link|alias]]) const rawLink = node.value.trim().split("|")[0].trim(); // e.g., '../f1/test' // Resolve the absolute path based on the current file's directory const folder = path.dirname(url.split(".")[0]); const absPath = path.join(folder, rawLink); // e.g., 'mdpath/f1/test' node.data.permalink = `/${absPath}.md`; node.data.hProperties.href = `/${absPath}.md`; }); }; } function unescapeHtml(html: string): string { return html .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'"); } function processContent(content: string): string { // Adjusting the regex to correctly match the pattern {@html ...} and removing the surrounding spaces. const regex = /{@html\s+([\s\S]*?)\s*}/g; const stage1 = content.replace(regex, (match, p1) => { // Removing {@html } and unescaping the inner HTML return unescapeHtml(p1); }); const backticksRemoved = stage1.replace( /`()`/g, (match, p1) => { return p1; // Remove the backticks around ... }, ); return unescapeHtml(backticksRemoved); } /** * Compile Markdown content using mdsvex with various plugins. */ async function compileMarkdown(fileContent: string, url: string): Promise { // Add or update frontmatter with the provided URL const processedContent = addFrontmatterToMarkdown(fileContent, url); const compiled: CompiledMD = await compile(processedContent, { extensions: [".svx"], smartypants: { dashes: "oldschool", }, remarkPlugins: [ remarkMath, remarkFootnotes, [ wikiLink, { hrefTemplate: (permalink: string) => `/${permalink}.md`, }, ], [customWikiLink], obsidianImagePlugin, remarkMermaid, remarkHighlight, remarkLogImages, remarkTags, ], rehypePlugins: [rehypeKatex, rehypeCallouts, rehypeAutolinkHeadings, rehypeWrapLiWithP], }); const frontmatter: Frontmatter = compiled.data?.fm || {}; const fileName: string = path.parse(fileContent).name; // Adjusted as fdpath is now url const title: string = frontmatter.title || fileName; // const node = { code: processContent(compiled.code), data: compiled.data }; const node = {code: processedContent, data: processedContent}; // Use the provided URL return { compiled: node, title, url }; } async function ensureMdbaseCollection() { try { // Check if the collection exists const collection = await pb.collections.getOne("mdbase"); console.log("Mdbase collection already exists"); const linksFieldExists = collection.schema.some( (field) => field.name === "links", ); const backlinksFieldExists = collection.schema.some( (field) => field.name === "backlinks", ); const tagFieldExists = collection.schema.some( (field) => field.name === "tags", ); if (!linksFieldExists) { console.log("Adding links field to mdbase collection..."); await pb.collections.update("mdbase", { schema: [ ...collection.schema, { name: "links", type: "relation", required: false, options: { collectionId: collection.id, cascadeDelete: false, maxSelect: null, displayFields: ["title"], }, }, ], }); console.log("Links field added successfully"); } else { console.log("Links field already exists"); } if (!backlinksFieldExists) { console.log("Adding links field to mdbase collection..."); await pb.collections.update("mdbase", { schema: [ ...collection.schema, { name: "backlinks", type: "relation", required: false, options: { collectionId: collection.id, cascadeDelete: false, maxSelect: null, displayFields: ["title"], }, }, ], }); console.log("Backlinks field added successfully"); } else { console.log("BackLinks field already exists"); } if (!tagFieldExists) { const tagcol = await pb.collections.getOne("tags"); console.log("Adding links field to mdbase collection..."); await pb.collections.update("mdbase", { schema: [ ...collection.schema, { name: "tags", type: "relation", required: false, options: { collectionId: tagcol.id, cascadeDelete: false, maxSelect: null, displayFields: ["title"], }, }, ], }); console.log("Tags field added successfully"); } else { console.log("Tags field already exists"); } // Check if the index exists const indexExists = collection.indexes.some((index) => index.includes("CREATE INDEX `idx_url` ON `mdbase` (`url`)"), ); if (!indexExists) { console.log("Creating index on url field..."); await pb.collections.update("mdbase", { indexes: [ ...collection.indexes, "CREATE INDEX `idx_url` ON `mdbase` (`url`)", ], }); console.log("Index created successfully"); } else { console.log("Index on url field already exists"); } } catch (error) { if (error.status === 404) { console.log("Mdbase collection does not exist. Creating..."); try { await pb.collections.create({ name: "mdbase", type: "base", schema: [ { name: "title", type: "text" }, { name: "content", type: "text" }, { name: "url", type: "text" }, { name: "mdfile", type: "file", required: true, options: { maxSelect: 1, maxSize: 5242880, // 5MB max size }, }, { name: "frontmatter", type: "json", options: { maxSize: 5242880, }, }, ], indexes: ["CREATE INDEX `idx_url` ON `mdbase` (`url`)"], }); console.log( "Mdbase collection created successfully with index on url field", ); } catch (createError) { console.error("Failed to create mdbase collection:", createError); throw createError; } } else { console.error("Error checking mdbase collection:", error); throw error; } } } function extractWikiLinks(htmlContent: string): string[] { // const regex = /]+href="([^"]+\.md)"[^>]*>/g; // const matches = htmlContent.matchAll(regex); // return Array.from(matches, (m) => m[1]); return [] } async function updateLinks(recordId: string, content: string) { const mdbaseCollection = pb.collection("mdbase"); // Extract wiki links from the content const wikiLinks = extractWikiLinks(content); console.log("WIKI =====>", wikiLinks); // Get the current record const currentRecord = await mdbaseCollection.getOne(recordId, { expand: "links,backlinks", }); const oldLinks = currentRecord.links || []; // Get IDs of linked documents const newLinkedRecordIds = []; for (const link of wikiLinks) { const linkedRecords = await mdbaseCollection.getList(1, 1, { filter: `url="${link.startsWith("/") ? link.slice(1) : link}"`, }); if (linkedRecords.items.length > 0) { newLinkedRecordIds.push(linkedRecords.items[0].id); } } // Update the current record's links await mdbaseCollection.update(recordId, { links: newLinkedRecordIds, }); // Update backlinks const linksToAdd = newLinkedRecordIds.filter((id) => !oldLinks.includes(id)); const linksToRemove = oldLinks.filter( (id) => !newLinkedRecordIds.includes(id), ); for (const id of linksToAdd) { await mdbaseCollection.update(id, { "backlinks+": recordId, }); } for (const id of linksToRemove) { await mdbaseCollection.update(id, { "backlinks-": recordId, }); } } async function ensureAttachmentsCollection() { try { // Check if the collection exists await pb.collections.getOne("attachments"); console.log("Attachments collection already exists"); } catch (error) { if (error.status === 404) { console.log("Attachments collection does not exist. Creating..."); try { await pb.collections.create({ name: "attachments", type: "base", schema: [ { name: "file", type: "file", required: true, options: { maxSelect: 1, maxSize: 524288000, }, }, { name: "url", type: "text", required: true, }, ], }); console.log("Attachments collection created successfully"); } catch (createError) { console.error("Failed to create attachments collection:", createError); throw createError; } } else { console.error("Error checking attachments collection:", error); throw error; } } } async function ensureTagsCollection() { try { // Check if the collection exists await pb.collections.getOne("tags"); console.log("Tags collection already exists"); } catch (error) { if (error.status === 404) { console.log("Tags collection does not exist. Creating..."); const collection = await pb.collections.getOne("mdbase"); try { await pb.collections.create({ name: "tags", type: "base", schema: [ { name: "tag", type: "text", required: true, }, { name: "links", type: "relation", required: false, options: { collectionId: collection.id, cascadeDelete: false, maxSelect: null, displayFields: ["title"], }, }, ], }); console.log("tags collection created successfully"); } catch (createError) { console.error("Failed to create tags collection:", createError); throw createError; } } else { console.error("Error checking tags collection:", error); throw error; } } } interface Tag { id: string; name: string; links: string[]; } async function parseTagsAndUpdatePocketBase( compiledMarkdown: string, frontmatter: Frontmatter, noteId: string, pb: PocketBase, ) { const processedTags = new Set(); const noteTags: string[] = []; // Function to process a single tag async function processTag(tagName: string) { if (processedTags.has(tagName)) return; processedTags.add(tagName); try { let tag: Tag; try { tag = await pb.collection("tags").getFirstListItem(`tag="${tagName}"`); // If the tag exists, update it if (!tag.links.includes(noteId)) { await pb.collection("tags").update(tag.id, { links: [...tag.links, noteId], }); console.log( `Updated existing tag ${tagName} with new link to note ${noteId}`, ); } else { console.log(`Tag ${tagName} already linked to note ${noteId}`); } } catch (error) { // If the tag doesn't exist, create it tag = await pb.collection("tags").create({ tag: tagName, links: [noteId], }); console.log(`Created new tag: ${tagName}`); } noteTags.push(tag.id); } catch (error) { console.error(`Error processing tag "${tagName}":`, error); } } // Process tags from compiled markdown const tagRegex = /([^<]+)<\/span>/g; let match; while ((match = tagRegex.exec(compiledMarkdown)) !== null) { const tagName = match[1]; await processTag(tagName); } // Process tags from frontmatter if (frontmatter.tags && Array.isArray(frontmatter.tags)) { for (const tag of frontmatter.tags) { await processTag(tag); } } // Update the note in mdbase collection with the tags try { const note = await pb.collection("mdbase").getOne(noteId); await pb.collection("mdbase").update(noteId, { tags: noteTags, }); console.log(`Updated note ${noteId} with tags: ${noteTags.join(", ")}`); } catch (error) { console.error(`Error updating note ${noteId} with tags:`, error); } } export const POST: RequestHandler = async ({ request }) => { return apiKeyMiddleware(request, async () => { try { await ensureAttachmentsCollection(); await ensureMdbaseCollection(); await ensureTagsCollection(); const formData = await request.formData(); const file = formData.get("file") as File | null; const url = formData.get("url") as string | null; if (!file) { return new Response(JSON.stringify({ message: "No file uploaded" }), { status: 400, }); } const fileName = file.name; const fileExtension = path.extname(fileName).toLowerCase(); if (fileExtension === ".md") { if (!url) { return new Response( JSON.stringify({ message: "URL is required for Markdown files" }), { status: 400, }, ); } const fileContent = await file.text(); const { compiled, title, url: providedUrl, } = await compileMarkdown(fileContent, url); const frontmatter = compiled.data?.fm || {}; console.log("front", frontmatter); const mdbaseCollection = pb.collection("mdbase"); const existingRecords = await mdbaseCollection.getList(1, 1, { filter: `url="${providedUrl}"`, }); let record; if (existingRecords.items.length > 0) { const existingRecord = existingRecords.items[0]; record = await mdbaseCollection.update(existingRecord.id, { title, frontmatter, content: compiled.code, url: providedUrl, mdfile: file, }); } else { record = await mdbaseCollection.create({ title, frontmatter, content: compiled.code, url: providedUrl, mdfile: file, }); } // Update links and backlinks await updateLinks(record.id, compiled.code); console.log("Updated bi-directional links"); await parseTagsAndUpdatePocketBase( compiled.code, frontmatter, record.id, pb, ); console.log("Updated tags"); return new Response( JSON.stringify({ message: "Markdown file uploaded successfully", record, }), { status: 200 }, ); } else if ( [".png", ".jpg", ".svg", ".jpeg", ".gif", ".webp"].includes( fileExtension, ) ) { console.log("Processing image file..."); try { const attachmentsCollection = pb.collection("attachments"); // Check if a record with the given URL already exists const existingRecords = await attachmentsCollection.getList(1, 1, { filter: `url="${url}"`, }); let attachmentRecord; if (existingRecords.items.length > 0) { // Update existing record const existingRecord = existingRecords.items[0]; console.log("Updating existing attachment record..."); attachmentRecord = await attachmentsCollection.update( existingRecord.id, { file: file, url: url, }, ); } else { // Create new record console.log("Creating new attachment record..."); attachmentRecord = await attachmentsCollection.create({ file: file, url: url, }); } const fileUrl = pb.getFileUrl( attachmentRecord, attachmentRecord.file, ); console.log("Generated file URL:", fileUrl); return new Response( JSON.stringify({ message: existingRecords.items.length > 0 ? "Image updated successfully" : "Image uploaded successfully", record: attachmentRecord, url: fileUrl, }), { status: 200 }, ); } catch (imageError: any) { console.error("Error during image upload/update:", imageError); return new Response( JSON.stringify({ message: "Image upload/update failed", error: imageError.message || "Unknown error during image upload/update", details: imageError.data ? JSON.stringify(imageError.data) : "No additional details", }), { status: 500 }, ); } } else { return new Response( JSON.stringify({ message: "Unsupported file type" }), { status: 400 }, ); } } catch (error: any) { console.error("Error in upload API:", error); return new Response( JSON.stringify({ message: "File upload failed", error: error.message || "Unknown error", details: error.data ? JSON.stringify(error.data) : "No additional details", }), { status: 500 }, ); } }); }; export const DELETE: RequestHandler = async ({ request }) => { return apiKeyMiddleware(request, async () => { try { const { url } = await request.json(); if (!url) { return new Response(JSON.stringify({ message: "URL is required" }), { status: 400, }); } const mdbaseCollection = pb.collection("mdbase"); const attachmentsCollection = pb.collection("attachments"); let deletedCount = 0; // Try to delete from mdbase collection try { const mdbaseRecords = await mdbaseCollection.getList(1, 1, { filter: `url="${url}"`, }); if (mdbaseRecords.items.length > 0) { await mdbaseCollection.delete(mdbaseRecords.items[0].id); console.log(`Deleted ${url} from mdbase collection`); deletedCount++; } } catch (error) { console.error(`Error deleting ${url} from mdbase collection:`, error); } // Try to delete from attachments collection try { const attachmentRecords = await attachmentsCollection.getList(1, 1, { filter: `url="${url}"`, }); if (attachmentRecords.items.length > 0) { await attachmentsCollection.delete(attachmentRecords.items[0].id); console.log(`Deleted ${url} from attachments collection`); deletedCount++; } } catch (error) { console.error( `Error deleting ${url} from attachments collection:`, error, ); } if (deletedCount > 0) { return new Response( JSON.stringify({ message: "File deleted successfully" }), { status: 200, }, ); } else { return new Response(JSON.stringify({ message: "File not found" }), { status: 404, }); } } catch (error: any) { console.error("Error in delete API:", error); return new Response( JSON.stringify({ message: "File deletion failed", error: error.message || "Unknown error", details: error.data ? JSON.stringify(error.data) : "No additional details", }), { status: 500 }, ); } }); }; export const GET: RequestHandler = async ({ request }) => { return apiKeyMiddleware(request, async () => { try { await ensureAttachmentsCollection(); await ensureMdbaseCollection(); await ensureTagsCollection(); const mdbaseCollection = pb.collection("mdbase"); const attachmentsCollection = pb.collection("attachments"); const mdbaseRecords = await mdbaseCollection.getFullList({ fields: "url", }); const attachmentRecords = await attachmentsCollection.getFullList({ fields: "url", }); const allUrls = [ ...mdbaseRecords.map((record) => record.url), ...attachmentRecords.map((record) => record.url), ]; return new Response(JSON.stringify(allUrls), { status: 200, headers: { "Content-Type": "application/json" }, }); } catch (error: any) { console.error("Error in list API:", error); return new Response( JSON.stringify({ message: "Failed to list files", error: error.message || "Unknown error", details: error.data ? JSON.stringify(error.data) : "No additional details", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } }); }; // Handle OPTIONS requests for CORS preflight export const OPTIONS: RequestHandler = async () => { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, DELETE, OPTIONS, GET", "Access-Control-Allow-Headers": "Content-Type", }, }); }; ================================================ FILE: src/routes/login/+page.server.ts ================================================ import type { PageServerLoad, Actions } from './$types.js'; import { fail, redirect } from '@sveltejs/kit'; import { superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import { formSchema } from '$lib/components/schema'; export const load: PageServerLoad = async () => { return { form: await superValidate(zod(formSchema)) }; }; export const actions: Actions = { default: async (event) => { const data = await superValidate(event, zod(formSchema)); if (!data.valid) { return fail(400, { data }); } console.log(data); const email = data.data.username; const password = data.data.password; console.log('Login action called'); if (!email || !password) { return fail(400, { emailRequired: !email, passwordRequired: !password }); } try { const authData = await event.locals.pb.collection('users').authWithPassword(email, password); console.log('Logged in successfully. Auth state:', event.locals.pb.authStore.isValid); // Ensure the auth data is saved to the auth store event.locals.pb.authStore.save(authData.token, authData.record); // Set the auth cookie const cookieOptions = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 30 // 30 days }; const cookie = event.locals.pb.authStore.exportToCookie(cookieOptions); console.log('Auth state before redirect:', event.locals.pb.authStore.isValid); console.log('Attempting to redirect to /dashboard'); // Use throw redirect instead of return // throw redirect(303, '/login/success'); } catch (error) { console.error('Login error:', error); const errorObj = error as ClientResponseError; return fail(500, { form: data }); } throw redirect(303, '/login/success'); /* return { form }; */ } }; ================================================ FILE: src/routes/login/+page.svelte ================================================ ================================================ FILE: src/routes/login/success/+page.server.ts ================================================ import type { PageServerLoad } from './$types'; import { redirect } from '@sveltejs/kit'; export const load: PageServerLoad = async ({ locals }) => { console.log('Dashboard load function. Auth state:', locals.pb.authStore.isValid); if (!locals.pb.authStore.isValid) { console.log('User not authenticated, redirecting to login'); throw redirect(303, '/login'); } // You can fetch additional data for the dashboard here return { user: locals.pb.authStore.model }; }; ================================================ FILE: src/routes/login/success/+page.svelte ================================================

Welcome to your dashboard, {data.user?.email}

Logged in Successfully !!
================================================ FILE: src/routes/publications/+page.svelte ================================================
{#each pubs as pub (pub.title)}
{pub.title} {#if pub.award}
{pub.award}
{/if}
{pub.auth}
{pub.venue}
{/each}
================================================ FILE: src/routes/tags/[tag]/+page.server.ts ================================================ import PocketBase from 'pocketbase'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ params }) => { try { const pb = await getAuthenticatedPocketBase(); const record = await pb.collection('tags').getFirstListItem(`tag="${params.tag}"`, { expand: 'links' }); console.log('TAG =====>', record); // Extract the expanded 'links' data const posts = record.expand?.links?.map((link: any) => ({ id: link.id, title: link.title, url: link.url })) || []; return { tag: record.tag, posts: posts, error: null }; } catch (error) { console.error('Error fetching tag data:', error); return { tag: params.tag, posts: [], error: error instanceof Error ? error.message : 'An unknown error occurred' }; } }; ================================================ FILE: src/routes/tags/[tag]/+page.svelte ================================================
Tag: {data.tag}
Posts tagged with {data.tag}
{#each data.posts as post} {/each}
================================================ FILE: src/writing/+page.server.ts ================================================ import PocketBase from 'pocketbase'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; const pb = await getAuthenticatedPocketBase(); export async function load({ params }) { try { const mdbase = await pb.collections.getOne('mdbase'); const records = await pb.collection('mdbase').getList(1, 10, { sort: '-created' }); const posts = Object.values(records.items).map((item) => ({ title: item.title, id: item.id, date: item.created, url: item.url })); return { posts }; } catch (error) { return { posts: [], err: error }; } } ================================================ FILE: src/writing/+page.svelte ================================================
Latest
{#if data && data.posts.length > 0} {#each data.posts as p, id (id)}
{formatDate(p.date)}
{/each} {:else}
Please upload markdown files to begin
{/if}
================================================ FILE: src/writing/[...dir]/+page.server.ts ================================================ ================================================ FILE: src/writing/[...dir]/+page.svelte ================================================
This is a directory
================================================ FILE: src/writing/[...post].md/+page.server.ts ================================================ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import PocketBase from 'pocketbase'; import { promises as fs } from 'fs'; import { getAuthenticatedPocketBase } from '$lib/server/auth'; const pb = await getAuthenticatedPocketBase(); async function getBacklinks(url) { const mdbaseCollection = pb.collection('mdbase'); const documentUrl = url; try { if (!documentUrl) { return new Response(JSON.stringify({ message: 'URL parameter is required' }), { status: 400 }); } const documents = await mdbaseCollection.getList(1, 1, { filter: `url="${documentUrl}"`, expand: 'backlinks' }); if (documents.items.length === 0) { return new Response(JSON.stringify({ message: 'Document not found' }), { status: 404 }); } const document = documents.items[0]; const backLinks = (document.expand?.backlinks || []).map((link) => ({ id: link.id, title: link.title, url: link.url })); return backLinks; } catch (error: any) { console.error('Error in backlinks API:', error); return {}; } } async function computeGraphData(fileUrl) { const currentPage = await pb.collection('mdbase').getFirstListItem(`url="${fileUrl}"`); const relatedPages = await pb.collection('mdbase').getList(1, 50, { filter: `id ?~ "${currentPage.backlinks}" || id ?~ "${currentPage.links}"` }); // Use a Set to store unique node IDs const uniqueNodeIds = new Set([currentPage.id]); // Create nodes array with current page const nodes = [{ id: currentPage.id, label: currentPage.title, color: '#ff0000' }]; // Add related pages to nodes array, avoiding duplicates relatedPages.items.forEach((p) => { if (!uniqueNodeIds.has(p.id)) { uniqueNodeIds.add(p.id); nodes.push({ id: p.id, label: p.title, color: '#00ff00' }); } }); // Create edges array const edges = [ ...currentPage.links.map((link) => ({ from: currentPage.id, to: link })), ...currentPage.backlinks.map((backlink) => ({ from: backlink, to: currentPage.id })) ]; return { nodes, edges }; } // Main load function export async function load({ params, fetch, locals }) { try { // Step 1: Authenticate /* console.log(pb); */ console.log(params.post); const post = await pb.collection('mdbase').getFirstListItem(`url="${params.post}.md"`); const backlinks = await getBacklinks(`${params.post}.md`); const graphData = await computeGraphData(`${params.post}.md`); return { post, title: post.title, backlinks, graphData }; } catch (error) { console.error(`Failed to fetch post: ${error}`); return { message: `Failed to fetch post: ${error}` }; } } ================================================ FILE: src/writing/[...post].md/+page.svelte ================================================
{data.title}
{#if data?.backlinks?.length > 0}
BACKLINKS
{/if} {#each data?.backlinks || [] as bl (bl.id)} {/each}
================================================ FILE: start.sh ================================================ #!/bin/sh # Ensure the environment variables are correctly set echo "Creating admin with email: ${POCKETBASE_ADMIN_EMAIL}" echo "PocketBase URL: ${POCKETBASE_URL}" # Start PocketBase in the background /pb/pocketbase serve --http=0.0.0.0:8080 --dir /app/db & # Wait for PocketBase to start (adjust the sleep time if necessary) sleep 5 # Create the admin user using environment variables /pb/pocketbase admin create "${POCKETBASE_ADMIN_EMAIL}" "${POCKETBASE_ADMIN_PASSWORD}" --dir /app/db sleep 2 # Build the SvelteKit app (requires PocketBase to be running) npm run build # # Optional: Log environment variables for debugging # echo "PocketBase URL: ${POCKETBASE_URL}" # echo "API Key: ${API_KEY}" # echo "Title: ${TITLE}" ================================================ FILE: start_services.sh ================================================ #!/bin/sh # Ensure the environment variables are correctly set echo "Creating admin with email: ${POCKETBASE_ADMIN_EMAIL}" echo "PocketBase URL: ${POCKETBASE_URL}" # Start PocketBase in the background /pb/pocketbase serve --http=0.0.0.0:8080 --dir /app/db & # Wait for PocketBase to start (adjust the sleep time if necessary) sleep 5 # Create the admin user using environment variables /pb/pocketbase admin create "${POCKETBASE_ADMIN_EMAIL}" "${POCKETBASE_ADMIN_PASSWORD}" --dir /app/db sleep 2 npm run build # Build the SvelteKit app (requires PocketBase to be running) node build # # Optional: Log environment variables for debugging # echo "PocketBase URL: ${POCKETBASE_URL}" # echo "API Key: ${API_KEY}" # echo "Title: ${TITLE}" ================================================ FILE: static/fonts.css ================================================ @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Bold.ttf') format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-BoldItalic.ttf') format('truetype'); font-weight: bold; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ExtraLight.ttf') format('truetype'); font-weight: 200; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ExtraLightItalic.ttf') format('truetype'); font-weight: 200; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Italic.ttf') format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Light.ttf') format('truetype'); font-weight: 300; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-LightItalic.ttf') format('truetype'); font-weight: 300; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Medium.ttf') format('truetype'); font-weight: 500; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-MediumItalic.ttf') format('truetype'); font-weight: 500; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-SemiBold.ttf') format('truetype'); font-weight: 600; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-SemiBoldItalic.ttf') format('truetype'); font-weight: 600; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Text.ttf') format('truetype'); font-weight: 400; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-TextItalic.ttf') format('truetype'); font-weight: 400; font-style: italic; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-Thin.ttf') format('truetype'); font-weight: 100; font-style: normal; } @font-face { font-family: 'IBM Plex Sans'; src: url('/fonts/IBMPlexSans-ThinItalic.ttf') format('truetype'); font-weight: 100; font-style: italic; } @font-face { font-family: 'Megrim'; src: url('/fonts/Megrim-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } /* ---- IBM Plex Mono ---- */ @font-face { font-family: 'IBM Plex Mono'; font-weight: 700; font-style: normal; src: url('/fonts/IBMPlexMono-Bold.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 700; font-style: italic; src: url('/fonts/IBMPlexMono-BoldItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 200; font-style: normal; src: url('/fonts/IBMPlexMono-ExtraLight.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 200; font-style: italic; src: url('/fonts/IBMPlexMono-ExtraLightItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: italic; src: url('/fonts/IBMPlexMono-Italic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 300; font-style: normal; src: url('/fonts/IBMPlexMono-Light.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 300; font-style: italic; src: url('/fonts/IBMPlexMono-LightItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-style: normal; src: url('/fonts/IBMPlexMono-Medium.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-style: italic; src: url('/fonts/IBMPlexMono-MediumItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; src: url('/fonts/IBMPlexMono-Regular.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: normal; src: url('/fonts/IBMPlexMono-SemiBold.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: italic; src: url('/fonts/IBMPlexMono-SemiBoldItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; src: url('/fonts/IBMPlexMono-Text.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: italic; src: url('/fonts/IBMPlexMono-TextItalic.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 100; font-style: normal; src: url('/fonts/IBMPlexMono-Thin.ttf') format('truetype'); } @font-face { font-family: 'IBM Plex Mono'; font-weight: 100; font-style: italic; src: url('/fonts/IBMPlexMono-ThinItalic.ttf') format('truetype'); } /* Lombok */ @font-face { font-family: 'Lombok'; font-weight: 400; font-style: normal; src: url('/fonts/Lombok.otf') format('opentype'); } ================================================ FILE: supervisord.conf ================================================ [supervisord] nodaemon=true [program:pocketbase] command=/pb/pocketbase serve --http=0.0.0.0:8090 --dev --dir /app/db stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:node] command=/usr/local/bin/node build stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 ================================================ FILE: svelte.config.js ================================================ /* import adapter from '@sveltejs/adapter-auto'; */ import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors // for more information about preprocessors preprocess: vitePreprocess(), kit: { // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://kit.svelte.dev/docs/adapters for more information about adapters. adapter: adapter(), csrf: { checkOrigin: false, } } }; export default config; ================================================ FILE: tailwind.config.ts ================================================ import { fontFamily } from 'tailwindcss/defaultTheme'; import type { Config } from 'tailwindcss'; const config: Config = { darkMode: ['class'], content: ['./src/**/*.{html,js,svelte,ts}'], safelist: ['dark'], theme: { fontFamily: { sans: ['IBM Plex Sans'], body: ['IBM Plex Sans'], display: ['IBM Plex Sans'], serif: ['IBM Plex Serif'], mono: ['IBM Plex Mono'] }, container: { center: true, padding: '2rem', screens: { '2xl': '1400px' } }, extend: { colors: { carbongray: { 50: '#f4f4f4', 100: '#e0e0e0', 200: '#c6c6c6', 300: '#a8a8a8', 400: '#8d8d8d', 500: '#6f6f6f', 600: '#525252', 700: '#262626', 800: '#161616', 900: '#000000' }, carbonblue: { 50: '#ecf5ff', 100: '#d0e2ff', 200: '#a6c8ff', 300: '#77a9fe', 400: '#4589ff', 500: '#0e61fe', 600: '#0043ce', 700: '#012d9c', 800: '#001d6c', 900: '#001141' }, carbonborder: { 300: '#393939', 200: '#525252', 100: '#6f6f6f' }, border: 'hsl(var(--border) / )', input: 'hsl(var(--input) / )', ring: 'hsl(var(--ring) / )', background: 'hsl(var(--background) / )', foreground: 'hsl(var(--foreground) / )', primary: { DEFAULT: 'hsl(var(--primary) / )', foreground: 'hsl(var(--primary-foreground) / )' }, secondary: { DEFAULT: 'hsl(var(--secondary) / )', foreground: 'hsl(var(--secondary-foreground) / )' }, destructive: { DEFAULT: 'hsl(var(--destructive) / )', foreground: 'hsl(var(--destructive-foreground) / )' }, muted: { DEFAULT: 'hsl(var(--muted) / )', foreground: 'hsl(var(--muted-foreground) / )' }, accent: { DEFAULT: 'hsl(var(--accent) / )', foreground: 'hsl(var(--accent-foreground) / )' }, popover: { DEFAULT: 'hsl(var(--popover) / )', foreground: 'hsl(var(--popover-foreground) / )' }, card: { DEFAULT: 'hsl(var(--card) / )', foreground: 'hsl(var(--card-foreground) / )' } }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' }, fontFamily: { sans: [...fontFamily.sans] } } } }; export default config; ================================================ FILE: tsconfig.json ================================================ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler" } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // from the referenced tsconfig.json - TypeScript does not merge them in } ================================================ FILE: vite.config.ts ================================================ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()] });