Showing preview only (206K chars total). Download the full file or copy to clipboard to get everything.
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 <name>
```
Replace `<name>` with your desired virtual environment name. You can then activate the virtual environment
using:
```bash
source <name>
```
For conda, you can use
```bash
conda create -n <name> python==3.12
```
and activate it with
```bash
conda activate <name>
```
### 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]]

### external images
```markdown

```

## 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=<long alpha-numeric string"
POCKETBASE_ADMIN_EMAIL=test@example.com
POCKETBASE_ADMIN_PASSWORD=1234567890
TITLE=Markopolis
```
Next create a `docker-compose.yaml` file with the following:
```yaml
version: '3.8'
services:
sveltekit-pocketbase:
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
volumes:
- ./pb_data:/app/pb
```
Now you can deploy Markopolis by running `docker-compse up -d`
## Local Installation
Requirements: Python 3.12
Install:
```sh
pip install markopolis
```
### Configuration:
Create a config file as a YAML file in any location.
Set the `MARKOPOLIS_CONFIG_PATH` environment variable to point to the location of the config file.
The config file should specify the domain of the deployment including the protocol and
the api key. The api key should be the same as what you used for the deployment:
```yaml
domain: "https://your-domain.com"
```
I recommend using a python virtual environment for the local installation.
## Post-install
Initially since the server hasn't seen any markdown files the app will throw a 500 Error.
Check [[usage]] for details on how to setup Markopolis
================================================
FILE: docs/introduction.md
================================================
---
title: Hi,
date: 09-23-2024
---
Welcome to Markopolis, a self-hostable web app and API for managing Markdown knowledge gardens.
Markopolis renders markdown notes as `html` and exposes APIs for interacting with markdown files
to implement a custom eco system around your notes.
TLDR: Self-hosted version of Obsidian publish with future promises
> 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
================================================
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/rehype-callouts/dist/themes/github/index.css"
/>
<link
rel="stylesheet"
href="%sveltekit.assets%/fonts.css"
/>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script src="//unpkg.com/3d-force-graph"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
================================================
FILE: src/lib/components/FileTree.svelte
================================================
<script lang="ts">
import { onMount } from "svelte";
import { ChevronRight, ChevronDown, File } from "lucide-svelte";
interface FileNode {
id: string;
title: string;
name: string;
url: string;
children: FileNode[];
}
export let fileTree: FileNode[] = [];
export let node: FileNode | undefined = undefined;
export let isExpanded = false;
function toggleExpand() {
isExpanded = !isExpanded;
}
$: isFolder = node && node.children.length > 0;
</script>
{#if node}
<div class="file-node">
<div
class="flex cursor-pointer items-center rounded px-2 py-1 hover:bg-carbongray-100 dark:hover:bg-carbongray-700"
on:click={toggleExpand}
on:keydown={(e) => e.key === "Enter" && toggleExpand()}
role="button"
tabindex="0"
>
{#if isFolder}
{#if isExpanded}
<ChevronDown class="mr-1 h-4 w-4 text-gray-500" />
{:else}
<ChevronRight class="mr-1 h-4 w-4 text-gray-500" />
{/if}
{:else}
<File class="mr-1 h-4 w-4 text-gray-500 flex-shrink-0" />
{/if}
{#if isFolder}
<span class="font-semibold">{node.name}</span> <!-- Folder name -->
{:else if node.url}
<a href={`/${node.url}`} class="text-carbongray-800 hover:underline"
>{node.title}</a
>
{:else}
<span class="font-semibold">{node.title}</span>
<!-- Fallback for files without URLs -->
{/if}
</div>
{#if isFolder && isExpanded}
<div class="children ml-4 mt-1">
{#each node.children as childNode}
<svelte:self node={childNode} />
{/each}
</div>
{/if}
</div>
{:else}
<div class="file-tree">
{#each fileTree as rootNode}
<svelte:self node={rootNode} />
{/each}
</div>
{/if}
================================================
FILE: src/lib/components/GraphScene.svelte
================================================
<script lang="ts">
import { T } from '@threlte/core';
import { Grid, OrbitControls, interactivity } from '@threlte/extras';
import { spring } from 'svelte/motion';
import { Vector3 } from 'three';
interactivity();
const scale = spring(1);
let fromPosition = new Vector3(1, 1, 0);
let toPosition = new Vector3(2, 2, 0);
let edgeColor = 'red'; // Replace with your color logic
// Define some sample graph data
const nodes = [
{ id: 1, position: [0, 0, 0] },
{ id: 2, position: [2, 1, 1] },
{ id: 3, position: [-1, 2, -1] },
{ id: 4, position: [1, -1, 2] }
];
const edges = [
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 2, to: 4 },
{ from: 3, to: 4 }
];
const nodeRadius = 0.3;
const nodeColor = '#FE3D00';
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
on:create={({ ref }) => {
ref.lookAt(0, 0, 0);
}}
>
<OrbitControls />
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 7]} intensity={Math.PI} />
<T.AmbientLight intensity={0.3} />
<T.Group scale={$scale} on:pointerenter={() => scale.set(1.2)} on:pointerleave={() => scale.set(1)}>
{#each nodes as node}
<T.Mesh position={node.position}>
<T.SphereGeometry args={[nodeRadius]} />
<T.MeshStandardMaterial color={nodeColor} toneMapped={false} />
</T.Mesh>
{/each}
{#each edges as edge}
<T.Line points={[fromPosition, toPosition]} color={edgeColor} lineWidth={2} />
{/each}
</T.Group>
<Grid cellColor="#FE3D00" sectionColor="#FE3D00" />
================================================
FILE: src/lib/components/LoginForm.svelte
================================================
<script lang="ts">
import * as Form from '$lib/components/ui/form';
import { Input } from '$lib/components/ui/input';
import { formSchema, type FormSchema } from './schema';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import * as Card from '$lib/components/ui/card';
export let data: SuperValidated<Infer<FormSchema>>;
const form = superForm(data, {
validators: zodClient(formSchema)
});
const { form: formData, enhance } = form;
</script>
<Card.Root class="mx-auto my-32 h-fit w-[350px]">
<Card.Header>
<Card.Title>Login</Card.Title>
</Card.Header>
<Card.Content>
<form method="POST" use:enhance>
<Form.Field {form} name="username">
<Form.Control let:attrs>
<Form.Label>Username</Form.Label>
<Input {...attrs} bind:value={$formData.username} class="w-[250px]" />
</Form.Control>
<Form.Description>This is your public display name.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="password">
<Form.Control let:attrs>
<Form.Label>Password</Form.Label>
<Input {...attrs} bind:value={$formData.password} class="w-[250px]" />
</Form.Control>
<Form.Description>This is your public display name.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
</Card.Content>
</Card.Root>
================================================
FILE: src/lib/components/MDGraph.svelte
================================================
<script>
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import * as Card from '$lib/components/ui/card';
let graphElement;
let Graph;
// Random tree
const N = 300;
const gData = {
nodes: [...Array(N).keys()].map((i) => ({ id: i })),
links: [...Array(N).keys()]
.filter((id) => id)
.map((id) => ({
source: id,
target: Math.round(Math.random() * (id - 1))
})),
id: [...Array(N).keys()]
};
onMount(async () => {
if (browser) {
const ForceGraph3D = (await import('3d-force-graph')).default;
Graph = ForceGraph3D()(graphElement).graphData(gData).nodeLabel('id');
Graph.width(300);
Graph.height(300);
}
return () => {
if (Graph) {
Graph.pauseAnimation();
Graph._destructor();
}
};
});
</script>
<div class="fixed right-5 top-10 h-[300px] w-[300px]">
{#if browser}
<div bind:this={graphElement}></div>
{/if}
</div>
<style>
</style>
================================================
FILE: src/lib/components/MDsvexRenderer.svelte
================================================
<!-- MDsveXContentRenderer.svelte -->
<script lang="ts">
import { onMount, tick } from "svelte";
import { afterNavigate } from "$app/navigation";
import mermaid from "mermaid";
import hljs from "highlight.js"; // Import highlight.js
import SvelteMarkdown from "svelte-markdown";
// import 'highlight.js/styles/nnfx-dark.min.css'; // Import a default style for highlight.js
import "highlight.js/styles/default.css";
import { marked } from "marked";
import admonition from "marked-admonition-extension";
// import 'highlight.js/styles/github-dark.css';
import { browser } from "$app/environment";
import markedAlert from "marked-alert";
export let content: string;
marked.use(markedAlert());
$: parsedContent = marked(content);
// Function to initialize and render mermaid diagrams
const renderMermaid = async () => {
// Reinitialize Mermaid every time we want to render
mermaid.initialize({ startOnLoad: false });
await tick(); // Ensure DOM is updated
mermaid.run({
querySelector: ".mermaid", // Render all elements with the mermaid class
});
};
const updateHighlightTheme = () => {
if (browser) {
const isDarkMode = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
document.documentElement.classList.toggle("theme-dark", isDarkMode);
document.documentElement.classList.toggle("theme-light", !isDarkMode);
hljs.highlightAll();
}
};
const highlightCode = () => {
hljs.highlightAll();
};
onMount(async () => {
await renderMermaid();
hljs.highlightAll();
// updateHighlightTheme();
// if (browser) {
// const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// mediaQuery.addListener(updateHighlightTheme);
// return () => mediaQuery.removeListener(updateHighlightTheme);
// }
});
// Use the afterNavigate hook to re-render mermaid diagrams after every navigation
afterNavigate(async () => {
await renderMermaid();
// highlightCode();
});
</script>
<div class="text-lg leading-relaxed md:w-[700px]">
<SvelteMarkdown bind:source={parsedContent} />
</div>
<style>
:global(pre) {
@apply my-4 rounded bg-carbongray-100 p-2 shadow-sm dark:bg-carbongray-400;
max-width: 100%;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
:global(pre code) {
@apply font-mono text-base;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
:global(code) {
@apply font-mono text-sm;
max-width: 100%;
}
/* For inline code */
:global(p code) {
@apply bg-carbongray-100 rounded-sm px-1 py-0.5;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
:global(.mermaid) {
text-align: center;
}
:global(.callout) {
width: 100%;
}
:global(.mermaid) {
@apply sm:w-[80svw] md:w-[65svw] xl:w-[80svw];
}
:global(table) {
@apply my-8 w-full text-left text-sm text-gray-500 dark:text-gray-400 rtl:text-right;
}
:global(thead) {
@apply bg-gray-50 text-xs uppercase text-gray-700 dark:bg-carbongray-600 dark:text-gray-400;
}
:global(th) {
@apply px-6 py-3;
}
:global(tbody tr) {
@apply border-b bg-white dark:border-carbongray-600 dark:bg-carbongray-700;
}
:global(tbody td) {
@apply whitespace-nowrap border-l-0 border-r-0 border-t-0 p-4 px-6 align-middle text-xs;
}
:global(.task-list-item) {
@apply flex list-none items-center py-2;
}
:global(.task-list-item input[type="checkbox"]),
:global(input[type="checkbox"]) {
@apply mr-2 h-4 w-4 cursor-pointer appearance-none border bg-white transition-all duration-200 ease-in-out;
position: relative;
}
:global(.task-list-item input[type="checkbox"]:checked),
:global(input[type="checkbox"]:checked) {
@apply border-blue-500 bg-blue-500;
}
:global(.task-list-item input[type="checkbox"]:checked::before),
:global(input[type="checkbox"]:checked::before) {
content: "\2713";
@apply absolute text-xs font-bold text-white;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
:global(.task-list-item input[type="checkbox"]:disabled),
:global(input[type="checkbox"]:disabled) {
@apply cursor-not-allowed border-carbongray-800;
}
:global(.task-list-item input[type="checkbox"]:disabled:checked),
:global(input[type="checkbox"]:disabled:checked) {
@apply border-carbongray-700 bg-carbongray-100 dark:bg-carbongray-600;
}
:global(.task-list-item input[type="checkbox"]:disabled:checked::before),
:global(input[type="checkbox"]:disabled:checked::before) {
@apply text-gray-500 dark:text-carbongray-100;
}
:global(.task-list-item),
:global(input[type="checkbox"] + *) {
@apply select-none text-lg;
}
:global(.task-list-item input[type="checkbox"]:disabled ~ *),
:global(input[type="checkbox"]:disabled + *) {
@apply text-gray-400;
}
:global(li) {
@apply relative pb-2 pl-8 text-lg leading-relaxed;
@apply flex flex-col items-start justify-start;
}
/* Unordered list styles with centered dot */
:global(ul > li::before) {
content: "";
@apply absolute left-2 top-[35%] h-2 w-2 rounded-full bg-carbonblue-500;
}
/* Ordered list styles */
/* :global(ol > li::before) {
content: counter(list-counter);
counter-increment: list-counter;
@apply absolute left-0 top-[0.3em] flex h-5 w-5 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-500;
} */
:global(ol) {
counter-reset: list-counter;
list-style-type: none;
padding-left: 0;
}
:global(ol > li) {
counter-increment: list-counter;
@apply relative pb-2 pl-8 text-lg leading-relaxed;
@apply flex flex-col items-start justify-start;
}
:global(ol > li::before) {
content: counter(list-counter);
@apply absolute left-0 top-[0.3em] flex h-5 w-5 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-500;
}
/* Task list item specific adjustments */
:global(.task-list-item) {
@apply flex flex-row items-start pl-0;
}
:global(.task-list-item::before) {
content: none;
}
:global(.task-list-item input[type="checkbox"]) {
@apply mr-2 mt-1;
}
/* Checkbox styles */
:global(.task-list-item input[type="checkbox"]),
:global(input[type="checkbox"]) {
@apply h-4 w-4 cursor-pointer appearance-none rounded border border-gray-300 bg-white transition-all duration-200 ease-in-out;
position: relative;
}
:global(.task-list-item input[type="checkbox"]:checked),
:global(input[type="checkbox"]:checked) {
@apply border-blue-500 bg-blue-500;
}
:global(.task-list-item input[type="checkbox"]:checked::before),
:global(input[type="checkbox"]:checked::before) {
content: "\2713";
@apply absolute text-xs font-bold text-white;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
:global(.task-list-item input[type="checkbox"]:disabled),
:global(input[type="checkbox"]:disabled) {
@apply cursor-not-allowed border-gray-200 bg-gray-100;
}
:global(.task-list-item input[type="checkbox"]:disabled:checked),
:global(input[type="checkbox"]:disabled:checked) {
@apply border-gray-300 bg-gray-300;
}
:global(.task-list-item input[type="checkbox"]:disabled:checked::before),
:global(input[type="checkbox"]:disabled:checked::before) {
@apply text-white;
}
:global(img) {
max-width: 100%;
}
:global(blockquote) {
@apply relative p-4;
}
:global(blockquote::before) {
content: "";
@apply absolute -left-8 -top-2 h-20 w-20 bg-carbongray-100 dark:bg-carbongray-600; /* Positioning, size, and color */
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z" fill="currentColor"/></svg>');
}
:global(blockquote p) {
@apply relative z-10 text-xl font-light italic;
}
:global(.highlight) {
@apply bg-[#ef538c] p-0.5 text-carbongray-900;
}
:global(.tag a) {
@apply mx-0.5 rounded-sm bg-[#fedc69] p-0.5 text-carbongray-900;
}
:global(.hljs-light) {
--hljs-theme: initial;
}
:global(.hljs-dark) {
--hljs-theme: github-dark;
}
:global(.hljs) {
background: var(--hljs-theme);
}
</style>
================================================
FILE: src/lib/components/MarkdownGraph.svelte
================================================
<script>
import { onMount } from 'svelte';
import { Canvas } from '@threlte/core';
/* import Scene from '$lib/components/Scene.svelte'; */
import Scene from '$lib/components/GraphScene.svelte';
export let graphData;
let nodes = [];
let edges = [];
let simulation;
onMount(() => {
if (graphData) {
nodes = graphData.nodes.map((node) => ({ ...node, x: 0, y: 0, z: 0 }));
edges = graphData.edges.map((edge) => ({
source: nodes.find((node) => node.id === edge.from),
target: nodes.find((node) => node.id === edge.to)
}));
simulation = forceSimulation(nodes)
.force(
'link',
forceLink(edges)
.id((d) => d.id)
.distance(100)
)
.force('charge', forceManyBody().strength(-200))
.force('center', forceCenter(0, 0, 0));
simulation.on('tick', () => {
nodes = [...nodes];
edges = [...edges];
});
}
});
</script>
<div class="h-[200px]">
<div class="wrapper">
<Canvas>
<Scene />
</Canvas>
</div>
</div>
<style>
div.wrapper {
height: 100%;
}
div.description {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 10;
color: #fe3d00;
}
</style>
================================================
FILE: src/lib/components/Scene.svelte
================================================
<script lang="ts">
import { T } from '@threlte/core';
import { Grid, OrbitControls, interactivity } from '@threlte/extras';
import { spring } from 'svelte/motion';
interactivity();
const scale = spring(1);
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
on:create={({ ref }) => {
ref.lookAt(0, 0, 0);
}}
>
<OrbitControls />
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 7]} intensity={Math.PI} />
<T.AmbientLight intensity={0.3} />
<T.Group scale={$scale} on:pointerenter={() => scale.set(1.5)} on:pointerleave={() => scale.set(1)}>
<T.Mesh position.y={1}>
<T.SphereGeometry args={[1]} />
<T.MeshStandardMaterial color="#FE3D00" toneMapped={false} />
</T.Mesh>
</T.Group>
<Grid cellColor="#FE3D00" sectionColor="#FE3D00" />
================================================
FILE: src/lib/components/SearchComponent.svelte
================================================
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { debounce } from 'lodash-es';
import { Search, Loader, X } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Dialog from '$lib/components/ui/dialog/index.js';
interface SearchResult {
title: string;
url: string;
snippet: string;
}
let searchQuery = '';
let searchResults: SearchResult[] = [];
let isLoading = false;
let isOpen = false;
let selectedIndex = -1;
const debouncedSearch = debounce(async () => {
if (searchQuery.trim() === '') {
searchResults = [];
return;
}
isLoading = true;
selectedIndex = -1;
try {
const response = await fetch(`/api/search?query=${encodeURIComponent(searchQuery)}`);
if (response.ok) {
const data = await response.json();
searchResults = Array.isArray(data) ? data : [];
} else {
console.error('Search failed:', await response.text());
searchResults = [];
}
} catch (error) {
console.error('Search error:', error);
searchResults = [];
} finally {
isLoading = false;
}
}, 300);
$: {
if (isOpen && searchQuery.trim() !== '') {
debouncedSearch();
}
}
function toggleSearch() {
isOpen = !isOpen;
if (!isOpen) {
clearSearch();
}
}
function clearSearch() {
searchQuery = '';
searchResults = [];
selectedIndex = -1;
}
function handleResultClick(url: string) {
isOpen = false;
clearSearch();
window.location.href = `/${url}`;
}
function highlightMatch(text: string, query: string) {
if (!query.trim()) return text;
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(
regex,
'<mark class="bg-yellow-200 text-gray-900 rounded px-0.5">$1</mark>'
);
}
function handleKeydown(event: KeyboardEvent) {
if (!isOpen) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
selectedIndex = (selectedIndex + 1) % searchResults.length;
break;
case 'ArrowUp':
event.preventDefault();
selectedIndex = (selectedIndex - 1 + searchResults.length) % searchResults.length;
break;
case 'Enter':
if (selectedIndex >= 0 && selectedIndex < searchResults.length) {
event.preventDefault();
handleResultClick(searchResults[selectedIndex].url);
}
break;
case 'Escape':
event.preventDefault();
toggleSearch();
break;
}
}
onMount(() => {
const handleGlobalKeydown = (event: KeyboardEvent) => {
if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
toggleSearch();
}
};
window.addEventListener('keydown', handleGlobalKeydown);
return () => {
window.removeEventListener('keydown', handleGlobalKeydown);
};
});
</script>
<svelte:window on:keydown={handleKeydown} />
<Dialog.Root bind:open={isOpen} on:close={clearSearch}>
<Dialog.Trigger>
<Button variant="ghost" size="icon" on:click={toggleSearch} aria-label="Open search">
<Search size={20} />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[560px]">
<Dialog.Header>
<Dialog.Title>Search</Dialog.Title>
</Dialog.Header>
<div class="relative">
<Input type="text" bind:value={searchQuery} placeholder="Search..." class="pr-10" />
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
{#if isLoading}
<Loader size={20} class="animate-spin text-gray-400" />
{:else if searchQuery}
<button
on:click={clearSearch}
class="text-gray-400 hover:text-gray-600"
aria-label="Clear search"
>
<X size={20} />
</button>
{:else}
<Search size={20} class="text-gray-400" />
{/if}
</div>
</div>
{#if searchResults.length > 0}
<div class="mt-4 max-h-[calc(60vh-2rem)] divide-y divide-carbongray-700 overflow-y-auto">
{#each searchResults as result, index}
<div
class="p-2 py-3 transition-colors duration-150 ease-in-out hover:bg-carbongray-700 {index ===
selectedIndex
? 'bg-blue-50'
: ''}"
>
<button
on:click={() => handleResultClick(result.url)}
class="w-full rounded text-left focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<h3 class="mb-1 text-lg font-semibold">
{@html highlightMatch(result.title, searchQuery)}
</h3>
<p class="line-clamp-2 text-sm">
{@html highlightMatch(result.snippet, searchQuery)}
</p>
</button>
</div>
{/each}
</div>
{:else if searchQuery && !isLoading}
<p class="mt-4 text-center text-sm text-gray-500">No results found</p>
{/if}
</Dialog.Content>
</Dialog.Root>
================================================
FILE: src/lib/components/Sidebar.svelte
================================================
<script lang="ts">
import { Sun, Moon, User } from 'lucide-svelte';
import { toggleMode } from 'mode-watcher';
import FileTree from '$lib/components/FileTree.svelte';
import SearchComponent from '$lib/components/SearchComponent.svelte';
import { Button } from '$lib/components/ui/button/index.js';
export let title;
export let captions;
</script>
<div class="mx-auto my-6 flex w-5/6 flex-col justify-start gap-3">
<div class="stitle flex-shrink text-4xl uppercase text-carbonblue-400">
{title}
</div>
<div class="gap-0.3 mb-8 flex flex-col justify-start text-sm">
<div>{captions[0]} / {captions[1]}</div>
<div>{captions[2]}</div>
</div>
<Button
on:click={toggleMode}
variant="ghost"
size="icon"
class="absolute right-2 top-2 h-[15px] w-[15px]"
>
<Sun
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">Toggle theme</span>
</Button>
<slot></slot>
</div>
<style>
.stitle {
font-family: 'Lombok';
}
</style>
================================================
FILE: src/lib/components/TagBar.svelte
================================================
<script lang="ts">
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { Tags } from "lucide-svelte";
import { Button } from "$lib/components/ui/button";
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js";
export let tags;
let hidden = true;
function toggle() {
hidden = !hidden;
}
beforeNavigate(() => {
hidden = true;
});
</script>
<Button variant="ghost" size="icon" on:click={toggle}>
<Tags />
</Button>
<ScrollArea
class={`${hidden ? "hidden" : ""} fixed right-4 top-9 h-[500px] w-[200px] rounded-sm bg-carbongray-50 dark:bg-carbongray-700 p-2 shadow`}
>
<div class="flex flex-col justify-center gap-2 p-2">
<div class="my-2 text-base font-bold">TAGS</div>
{#each tags as tag (tag.name)}
<div>
<a
class="text-primary hover:bg-carbongray-100 w-full p-1 rounded-sm dark:hover:bg-carbongray-600"
href="/tags/{tag.name}">#{tag.name}</a
>
</div>
{/each}
</div>
</ScrollArea>
================================================
FILE: src/lib/components/TopBar.svelte
================================================
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Menu } from 'lucide-svelte';
import { isSidebarVisible } from '$lib/stores/sidebarStore';
function toggleSidebar() {
isSidebarVisible.update((value) => !value);
}
export let title;
</script>
<div class="absolute left-0 top-0 z-30 flex h-[40px] items-center justify-between md:hidden">
<Button variant="ghost" size="icon" on:click={toggleSidebar}
><Menu class="text-carbongray-300" /></Button
>
{#if title}
<div class="title text-2xl">{title}</div>
{/if}
</div>
<style>
.title {
font-family: Megrim;
}
</style>
================================================
FILE: src/lib/components/award.svelte
================================================
<script>
export let starColor = '#000';
export let size = 30;
</script>
<svg
width="{size}px"
height="{size}px"
version="1.1"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={starColor}
d="m30.891 74.191h0.56641v-1.8633c0-1.5508 0.63281-2.9648 1.6562-3.9844 1.0195-1.0195 2.4297-1.6562 3.9844-1.6562h0.54688l-9.1719-41.957c-0.23438-1.0781 0.45312-2.1406 1.5312-2.375 0.29297-0.0625 0.58594-0.058594 0.85938 0.003906 6.2734 1.2227 13.332-0.28516 20.129-3.2578 8.4766-3.707 16.5-9.668 22.156-15.496 0.76953-0.78906 2.0312-0.80859 2.8203-0.039063 0.52734 0.51172 0.70703 1.2422 0.54688 1.9062l-15.148 61.211h0.47656c1.5469 0 2.957 0.63672 3.9805 1.6562 1.0273 1.0273 1.6641 2.4375 1.6641 3.9844v1.8633h0.56641c2.0547 0 3.9219 0.83984 5.2734 2.1914 1.3516 1.3516 2.1914 3.2227 2.1914 5.2734v13.344c0 1.1055-0.89453 2-2 2h-48.09c-1.1055 0-2-0.89453-2-2v-13.344c0-2.0547 0.83984-3.9219 2.1914-5.2734l0.12109-0.11328c1.3438-1.2852 3.1602-2.0781 5.1523-2.0781zm20.34-45.363 2.9062 5.4062 6.0312 1.0938c1.082 0.19531 1.8008 1.2305 1.6094 2.3125-0.070313 0.39844-0.25781 0.74609-0.51562 1.0195l-4.2422 4.4414 0.82422 6.082c0.14453 1.0898-0.62109 2.0977-1.7109 2.2422-0.40234 0.054688-0.79297-0.015625-1.1328-0.17969l-5.5273-2.6602-5.5273 2.6641c-0.99219 0.47656-2.1875 0.0625-2.6641-0.92969-0.18359-0.37891-0.23438-0.78906-0.17188-1.1758l0.82031-6.0391-4.2461-4.4375c-0.75781-0.79688-0.72656-2.0625 0.070312-2.8203 0.31641-0.30078 0.71094-0.47656 1.1133-0.53125l5.9414-1.0742 2.9062-5.4062c0.51953-0.96875 1.7344-1.332 2.7031-0.8125 0.35938 0.19141 0.63281 0.48047 0.8125 0.8125zm-0.15234 8.1406-1.6055-2.9805-1.6055 2.9805c-0.28125 0.51562-0.78516 0.90625-1.4062 1.0156l-3.332 0.60547 2.3477 2.457c0.40234 0.42188 0.61719 1.0195 0.53125 1.6406l-0.45703 3.3594 3.0508-1.4688c0.52734-0.25391 1.1641-0.27344 1.7344 0l3.0508 1.4688-0.45312-3.3594c-0.078125-0.57812 0.097656-1.1875 0.53125-1.6406l2.3477-2.457-3.2305-0.58594c-0.61719-0.078125-1.1914-0.44531-1.5078-1.0352zm-11.797 47.348c-1.1055 0-2-0.89453-2-2 0-1.1055 0.89453-2 2-2h20.383c1.1055 0 2 0.89453 2 2 0 1.1055-0.89453 2-2 2zm0 6.5586c-1.1055 0-2-0.89453-2-2s0.89453-2 2-2h20.383c1.1055 0 2 0.89453 2 2s-0.89453 2-2 2zm2.457-24.188h15.52l13.77-55.637c-5.3086 4.5273-11.734 8.7773-18.438 11.711-6.4883 2.8359-13.262 4.4492-19.598 3.9219l8.7461 40.008zm-6.2773 7.5039h28.027v-1.8633c0-0.45312-0.18359-0.86719-0.47656-1.1602-0.29688-0.29297-0.71094-0.47656-1.1641-0.47656h-24.75c-0.44922 0-0.85938 0.18359-1.1562 0.48047s-0.48047 0.70703-0.48047 1.1562zm32.594 4h-37.16c-0.91797 0-1.75 0.35938-2.3672 0.93359l-0.078125 0.082031c-0.62891 0.62891-1.0195 1.4922-1.0195 2.4453v11.34h44.086v-11.34c0-0.94922-0.39062-1.8203-1.0195-2.4453-0.62891-0.62891-1.4922-1.0195-2.4453-1.0195z"
/>
</svg>
================================================
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
================================================
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>
================================================
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<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["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
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>
================================================
FILE: src/lib/components/ui/card/card-description.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<p class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
<slot />
</p>
================================================
FILE: src/lib/components/ui/card/card-footer.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>
================================================
FILE: src/lib/components/ui/card/card-header.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 p-6", className)} {...$$restProps}>
<slot />
</div>
================================================
FILE: src/lib/components/ui/card/card-title.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let tag: $$Props["tag"] = "h3";
export { className as class };
</script>
<svelte:element
this={tag}
class={cn("font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>
================================================
FILE: src/lib/components/ui/card/card.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={cn("bg-card text-card-foreground rounded-xl border shadow", className)}
{...$$restProps}
on:click
on:focusin
on:focusout
on:mouseenter
on:mouseleave
>
<slot />
</div>
================================================
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
================================================
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import Cross2 from "svelte-radix/Cross2.svelte";
import * as Dialog from "./index.js";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DialogPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 200,
};
export { className as class };
</script>
<Dialog.Portal>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
className
)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<Cross2 class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>
================================================
FILE: src/lib/components/ui/dialog/dialog-description.svelte
================================================
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Description>
================================================
FILE: src/lib/components/ui/dialog/dialog-footer.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
</div>
================================================
FILE: src/lib/components/ui/dialog/dialog-header.svelte
================================================
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>
================================================
FILE: src/lib/components/ui/dialog/dialog-overlay.svelte
================================================
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
</script>
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
{...$$restProps}
/>
================================================
FILE: src/lib/components/ui/dialog/dialog-portal.svelte
================================================
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
type $$Props = DialogPrimitive.PortalProps;
</script>
<DialogPrimitive.Portal {...$$restProps}>
<slot />
</DialogPrimitive.Portal>
================================================
FILE: src/lib/components/ui/dialog/dialog-title.svelte
================================================
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Title
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Title>
================================================
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
================================================
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>
================================================
FILE: src/lib/components/ui/form/form-description.svelte
================================================
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-muted-foreground text-[0.8rem]", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>
================================================
FILE: src/lib/components/ui/form/form-element-field.svelte
================================================
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLDivElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>
================================================
FILE: src/lib/components/ui/form/form-field-errors.svelte
================================================
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-destructive text-[0.8rem] font-medium", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>
================================================
FILE: src/lib/components/ui/form/form-field.svelte
================================================
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>
================================================
FILE: src/lib/components/ui/form/form-fieldset.svelte
================================================
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>
================================================
FILE: src/lib/components/ui/form/form-label.svelte
================================================
<script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { Label } from "$lib/components/ui/label/index.js";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
let className: $$Props["class"] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
</script>
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>
================================================
FILE: src/lib/components/ui/form/form-legend.svelte
================================================
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>
================================================
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 extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};
================================================
FILE: src/lib/components/ui/input/input.svelte
================================================
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>
================================================
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
================================================
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
>
<slot />
</LabelPrimitive.Root>
================================================
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
================================================
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
orientation?: "vertical" | "horizontal";
};
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "vertical";
export { className as class };
</script>
<ScrollAreaPrimitive.Scrollbar
{orientation}
class={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px",
className
)}
>
<slot />
<ScrollAreaPrimitive.Thumb
class={cn("bg-border relative rounded-full", orientation === "vertical" && "flex-1")}
/>
</ScrollAreaPrimitive.Scrollbar>
================================================
FILE: src/lib/components/ui/scroll-area/scroll-area.svelte
================================================
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { Scrollbar } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = ScrollAreaPrimitive.Props & {
orientation?: "vertical" | "horizontal" | "both";
scrollbarXClasses?: string;
scrollbarYClasses?: string;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let orientation = "vertical";
export let scrollbarXClasses: string = "";
export let scrollbarYClasses: string = "";
</script>
<ScrollAreaPrimitive.Root {...$$restProps} class={cn("relative overflow-hidden", className)}>
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
<ScrollAreaPrimitive.Content>
<slot />
</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
{#if orientation === "vertical" || orientation === "both"}
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if}
{#if orientation === "horizontal" || orientation === "both"}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if}
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
================================================
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
================================================
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SeparatorPrimitive.Props;
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "horizontal";
export let decorative: $$Props["decorative"] = undefined;
export { className as class };
</script>
<SeparatorPrimitive.Root
class={cn(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className
)}
{orientation}
{decorative}
{...$$restProps}
/>
================================================
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<string, any> || {};
// 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: `<sup id="fnref${footnoteNumber}"><a href="#fn${footnoteNumber}" aria-describedby="footnote">${footnoteNumber}</a></sup>`,
};
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: '<section class="footnotes"><ol>',
};
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: `<li id="fn${footnoteNumber}">${content} <a href="#fnref${footnoteNumber}" aria-label="Back to content">↩︎</a></li>`,
};
tree.children.push(footnoteItem);
});
// Close the ordered list and section
tree.children.push({
type: 'html',
value: '</ol></section>',
});
};
}
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 = `<div class="mermaid">${node.value}</div>`;
}
});
};
};
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, number | string | undefined>
): 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
================================================
<script lang="ts">
import '../app.css';
import { ModeWatcher } from 'mode-watcher';
import { Separator } from '$lib/components/ui/separator';
import Sidebar from '$lib/components/Sidebar.svelte';
import FileTree from '$lib/components/FileTree.svelte';
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Menu, Grip, SquareX } from 'lucide-svelte';
import { beforeNavigate } from '$app/navigation';
import SearchComponent from '$lib/components/SearchComponent.svelte';
import TagBar from '$lib/components/TagBar.svelte';
export let data;
let showSidebar = false;
let sidebarRef;
let toggleButtonRef;
$: fileTree = data?.filetree;
$: siteTitle = data?.siteTitle;
$: tags = data?.tags;
// Toggle sidebar visibility
function toggleSidebar() {
showSidebar = !showSidebar;
}
// Close sidebar when clicking outside
function handleClickOutside(event) {
console.log('clicked');
if (
sidebarRef &&
!sidebarRef.contains(event.target) &&
toggleButtonRef &&
!toggleButtonRef.contains(event.target)
) {
showSidebar = false;
}
}
onMount(() => {
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
});
beforeNavigate(() => {
showSidebar = false;
});
</script>
<!-- Toggle button to control sidebar visibility -->
<div
bind:this={toggleButtonRef}
class="fixed left-0 top-0 flex w-[100%] flex-col items-start justify-center gap-2 bg-background lg:hidden"
>
<div class="flex w-[100%] items-center justify-between">
<div class="flex items-center justify-start">
<Button class="z-20" on:click={toggleSidebar} variant="ghost" size="icon">
<Grip />
</Button>
<div class="title text-2xl break-words">{siteTitle}</div>
</div>
<div class="gap-0.1 flex">
<TagBar {tags} />
<SearchComponent />
</div>
</div>
</div>
<div class="fixed right-0 top-0 hidden w-[100%] items-center justify-end bg-background lg:flex">
<TagBar {tags} />
<SearchComponent />
</div>
<!-- Sidebar -->
<div
bind:this={sidebarRef}
class={`fixed left-0 top-0 z-30 flex h-svh w-72 justify-between bg-background transition-transform lg:translate-x-0 ${showSidebar ? '' : '-translate-x-full'}`}
>
<Sidebar title={siteTitle} captions={data.captions}>
{#if fileTree}
<FileTree bind:fileTree />
{:else}
Loading...
{/if}
</Sidebar>
<Separator orientation="vertical" class="h-full"></Separator>
</div>
<!-- Main content -->
<div class="z-10 w-[100%] p-2 pt-12 lg:pl-80 lg:pt-6">
<ModeWatcher />
<slot></slot>
</div>
<style>
/* Ensure smooth transition when showing/hiding the sidebar */
.transition-transform {
transition: transform 0.2s ease-in-out;
}
.translate-x-full {
transform: translateX(-100%);
}
.title {
font-family: Megrim;
}
</style>
================================================
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
================================================
<script lang="ts">
import MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';
import MDGraph from '$lib/components/MDGraph.svelte';
import { CalendarDays } from 'lucide-svelte';
export let data: { content: string };
$: content = data.post?.content;
$: tags = data?.tags;
$: date = data.post?.created;
</script>
{#if data.post}
<div class="mb-10 mt-6 text-wrap md:w-[700px]">
<div class="my-4 text-6xl md:text-8xl">
{data.title}
</div>
<div class="flex flex-col justify-center gap-1">
{#if data?.post?.frontmatter?.date}
<div class="flex items-center gap-1">
<CalendarDays class="text-carbongray-400" size={15} />
<div class="text-base text-carbongray-400">{data.post.frontmatter.date.split(' ')[0]}</div>
</div>
{/if}
{#if tags}
<div class="flex flex-wrap gap-1">
{#each tags as tag (tag.name)}
<span class="mx-0.5 rounded-sm bg-[#fedc69] p-0.5 text-carbongray-900"
><a class="text-carbongray-900 dark:text-carbongray-900" href="/tags/{tag.name}"
>{tag.name}</a
></span
>
{/each}
</div>
{/if}
</div>
<hr class="mt-6" />
</div>
<MDsvexRenderer bind:content />
<div>
{#if data?.backlinks?.length > 0}
<hr class="my-4 w-[700px]" />
<div class="mb-2 mt-4 text-2xl font-light">BACKLINKS</div>
{/if}
{#each data?.backlinks || [] as bl (bl.id)}
<div><a href={`/${bl.url.trim()}`} class="text-large">{bl.title}</a></div>
{/each}
</div>
{:else}
<div class="mb-10 mt-6 text-wrap md:w-[700px]">
Set homepage in the frontmatter of one of your markdown files to display it as home
</div>
{/if}
<style>
:global(.tag a) {
@apply mx-0.5 bg-[#fedc69] p-1 text-carbongray-900;
}
</style>
================================================
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
================================================
<script lang="ts">
import MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';
import MDGraph from '$lib/components/MDGraph.svelte';
import { CalendarDays } from 'lucide-svelte';
export let data: { content: string };
$: content = data.post?.content;
$: tags = data?.tags;
$: date = data.post?.created;
</script>
<div class="mb-10 mt-6 text-wrap md:w-[700px]">
<div class="my-4 text-6xl md:text-8xl">
{data.title}
</div>
<div class="flex flex-col justify-center gap-1">
{#if data?.post?.frontmatter?.date}
<div class="flex items-center gap-1">
<CalendarDays class="text-carbongray-400" size={15} />
<div class="text-base text-carbongray-400">{data.post.frontmatter.date.split(' ')[0]}</div>
</div>
{/if}
{#if tags}
<div class="flex flex-wrap gap-1">
{#each tags as tag (tag.name)}
<span class="mx-0.5 rounded-sm bg-[#fedc69] p-0.5 text-carbongray-900"
><a class="text-carbongray-900 dark:text-carbongray-900" href="/tags/{tag.name}"
>{tag.name}</a
></span
>
{/each}
</div>
{/if}
</div>
<hr class="mt-6" />
</div>
<MDsvexRenderer bind:content />
<div>
{#if data?.backlinks?.length > 0}
<hr class="my-4 w-[700px]" />
<div class="mb-2 mt-4 text-2xl font-light">BACKLINKS</div>
{/if}
{#each data?.backlinks || [] as bl (bl.id)}
<div><a href={`/${bl.url.trim()}`} class="text-large">{bl.title}</a></div>
{/each}
</div>
<style>
:global(.tag a) {
@apply mx-0.5 bg-[#fedc69] p-1 text-carbongray-900;
}
</style>
================================================
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(
/`(<code[\s\S]*?<\/code>)`/g,
(match, p1) => {
return p1; // Remove the backticks around <code>...</code>
},
);
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 = /<a[^>]+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<string>();
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>(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>({
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 class="tag">([^<]+)<\/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
================================================
<script>
import LoginForm from '$lib/components/LoginForm.svelte';
export let data;
$: formData = data?.form;
</script>
<LoginForm bind:data={formData} />
================================================
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
================================================
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
export let data;
</script>
<h1>Welcome to your dashboard, {data.user?.email}</h1>
<div>Logged in Successfully !!</div>
================================================
FILE: src/routes/publications/+page.svelte
================================================
<script>
import Award from '$lib/components/award.svelte';
export const pubs = [
{
title:
'A Neuro-Symbolic Architecture for Efficient Classification using Hyperdimensional Computing',
auth: 'Rishikanth Chandrasekaran, Tajana Rosing',
venue: 'ISLPED',
date: 2024
},
{
title: 'Federated Hyperdimensional Computing',
auth: 'Kazim Ergun, Rishikanth Chandrasekaran, Tajana Rosing',
venue: 'ACM TIoT (under review)',
date: 2023
},
{
title: 'Multi-label classification with Hyperdimensional Representations',
auth: 'Rishikanth Chandrasekaran, F Asgareinjad, J Morris, T Rosing',
venue: 'IEEE Access Journal',
date: 2023
},
{
title: 'Fhdnn: Communication efficient and robust federated learning for aiot networks',
auth: 'R Chandrasekaran, K Ergun, J Lee, D Nanjunda, J Kang, T Rosing',
venue: 'Proceedings of the 59th ACM/IEEE Design Automation Conference',
date: 2022
},
{
title:
'Hdnn-pim: Efficient in memory design of hyperdimensional computing with feature extraction',
auth: 'A Dutta, S Gupta, B Khaleghi, R Chandrasekaran, W Xu, T Rosing',
venue: 'Proceedings of the Great Lakes Symposium on VLSI',
date: 2022
},
{
title: 'A drone-based system for intelligent and autonomous homes',
auth: 'S Xia, R Chandrasekaran, Y Liu, C Yang, TS Rosing, X Jiang',
venue: 'Proceedings of the 19th ACM Conference on Embedded Networked Sensor Systems',
date: 2021,
award: 'Best Demo'
},
{
title: 'Efficient Sparse Processing for Smart Home Applications',
auth: 'Rishikanth Chandrasekaran, Yunhui Guo, Anthony Thomas, Masimiliano Menarini, Michael Ostertag, Tajana Rosing',
venue: 'ACM Conference on Embedded Network Sensor Systems (SenSys)',
date: 2019
},
{
title:
'A Scalable System for Apportionment and Tracking of Energy Footprints in Commercial Buildings',
auth: 'Peter Wei, Xiaoqi Chen, Jordan Vega, Stephen Xia, Rishikanth Chandrasekaran, Xiaofan Jiang',
venue: 'ACM Transaction on Sensor Networks (TSON)',
date: 2018
},
{
title: 'PAWS: A Wearable Acoustic System for Pedestrian Safety',
auth: 'Daniel de Godoy, Bashima Islam, Stephen Xia, Md Tamzeed Islam, Rishikanth Chandrasekaran, Yen-Chun Chen, Shahriar Nirjon, Peter R Kinget, Xiaofan Jiang',
venue: 'Internet-of-Things Design and Implementation (IoTDI)',
date: 2018
},
{
title: 'Paws: A wearable acoustic system for pedestrian safety.',
venue:
'IEEE/ACM Third International Conference on Internet-of-Things Design and Implementation (IoTDI)',
date: 2018,
auth: 'Daniel de Godoy, Bashima Islam, Stephen Xia, Md Tamzeed Islam, Rishikanth Chandrasekaran, Yen-Chun Chen, Shahriar Nirjon, Peter R Kinget, Xiaofan Jiang'
},
{
title:
'ePrints a real-time and scalable system for fair apportionment and tracking of personal energy footprints in commercial buildings',
venue:
'ACM International Conference on Systems for Energy-Efficient Built Environments (BuildSys)',
auth: 'P Wei, X Chen, J Vega, S Xia, R Chandrasekaran, X Jiang',
date: 2017,
award: 'Best Paper Runner Up'
},
{
title: 'Adaptive and Personalized Energy Saving Suggestions for Occupants in Smart Buildings',
venue:
'ACM International Conference on Systems for Energy-Efficient Built Environments (BuildSys)',
auth: 'P Wei, X Chen, R Chandrasekaran, F Song, X Jiang',
date: 2016,
award: 'Best Poster'
},
{
title:
'SEUS: A Wearable Multi-Channel Acoustic Headset Platform to Improve Pedestrian Safety',
venue: 'ACM Conference on Embedded Network Sensor Systems (SenSys)',
auth: 'R Chandrasekaran, D de Godoy, S Xia, MT Islam, B Islam, S Nirjon, P Kinget, X Jiang',
date: 2016
},
{
title: 'Personal energy footprint in shared building environment',
venue: 'International Conference on Information Processing in Sensor Networks (IPSN)',
auth: 'P Wei, X Chen, R Chandrasekaran, F Song, X Jiang',
date: 2016
},
{
title:
'Low-cost intelligent gesture recognition engine for audio-vocally impaired individuals',
venue: 'Global Humanitarian Technology Conference (GHTC)',
auth: 'C Rishikanth, H Sekar, G Rajagopal, R Rajesh, V Vijayaraghavan',
date: 2014
}
];
</script>
<div class="flex w-[756px] flex-col justify-start gap-8">
{#each pubs as pub (pub.title)}
<div class="flex flex-col">
<div class="flex gap-2 text-lg text-gray-50">
{pub.title}
{#if pub.award}
<div class="flex flex-shrink-0 items-center gap-1">
<Award size="20" starColor="#0f62fe" />
<div class="text-sm">{pub.award}</div>
</div>
{/if}
</div>
<div class="text-sm">{pub.auth}</div>
<div class="text-xs italic text-blue-500">{pub.venue}</div>
</div>
{/each}
</div>
================================================
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
================================================
<script lang="ts">
export let data;
</script>
<div class="p-2 text-6xl font-bold lg:pt-6">
Tag: {data.tag}
</div>
<div class="my-8 p-2">
<div class="text-2xl">Posts tagged with {data.tag}</div>
<div class="items-left mt-4 flex flex-col gap-3">
{#each data.posts as post}
<div class="text-lg"><a href="/{post.url}">{post.title}</a></div>
{/each}
</div>
</div>
================================================
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
================================================
<script>
export let data;
function formatDate(dateString) {
const date = new Date(dateString);
// Format the date as DD MMM YYYY
return date.toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
}
</script>
<div class="mb-6 text-4xl font-bold">Latest</div>
<div class="flex flex-col items-start justify-center gap-4">
{#if data && data.posts.length > 0}
{#each data.posts as p, id (id)}
<div class="flex flex-col items-start justify-center gap-0.5">
<div class="text-xl"><a href="/writing/{p.url}">{p.title}</a></div>
<div class="text-sm text-gray-400">{formatDate(p.date)}</div>
</div>
{/each}
{:else}
<div class="text-lg">Please upload markdown files to begin</div>
{/if}
</div>
================================================
FILE: src/writing/[...dir]/+page.server.ts
================================================
================================================
FILE: src/writing/[...dir]/+page.svelte
================================================
<div>This is a directory</div>
================================================
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
================================================
<script lang="ts">
import MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';
import MDGraph from '$lib/components/MDGraph.svelte';
export let data: { content: string };
$: content = data.post?.content;
$: graphData = data.graphData;
</script>
<div class="mb-12 mt-6 text-wrap text-6xl md:w-[700px] md:text-8xl">
{data.title}
</div>
<MDsvexRenderer bind:content />
<div>
{#if data?.backlinks?.length > 0}
<div class="mb-2 mt-12 text-2xl font-light">BACKLINKS</div>
{/if}
{#each data?.backlinks || [] as bl (bl.id)}
<div><a href={`/writing/${bl.url.trim()}`} class="text-large">{bl.title}</a></div>
{/each}
</div>
================================================
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) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
},
secondary: {
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
},
destructive: {
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
},
muted: {
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
},
accent: {
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
},
popover: {
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
},
card: {
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
}
},
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()]
});
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
SYMBOL INDEX (52 symbols across 26 files)
FILE: src/app.d.ts
type Locals (line 5) | interface Locals {
FILE: src/lib/components/schema.ts
type FormSchema (line 8) | type FormSchema = typeof formSchema;
FILE: src/lib/components/ui/button/index.ts
type Variant (line 31) | type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size (line 32) | type Size = VariantProps<typeof buttonVariants>["size"];
type Props (line 34) | type Props = ButtonPrimitive.Props & {
type Events (line 39) | type Events = ButtonPrimitive.Events;
FILE: src/lib/components/ui/card/index.ts
type HeadingLevel (line 24) | type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
FILE: src/lib/components/ui/input/index.ts
type FormInputEvent (line 3) | type FormInputEvent<T extends Event = Event> = T & {
type InputEvents (line 6) | type InputEvents = {
FILE: src/lib/highlightCode.ts
function createHighlightStore (line 4) | function createHighlightStore() {
FILE: src/lib/md.ts
function addFrontmatterToMarkdown (line 12) | function addFrontmatterToMarkdown(fileContent: string, url: string): str...
FILE: src/lib/pbStore.ts
function login (line 17) | async function login(email: string, password: string) {
function logout (line 28) | function logout() {
FILE: src/lib/pocketbase.ts
function getAuthenticatedPocketBase (line 10) | async function getAuthenticatedPocketBase() {
FILE: src/lib/remark-plugins/footNotes.js
function remarkFootnoteHTML (line 5) | function remarkFootnoteHTML() {
FILE: src/lib/remark-plugins/highlightSyn.js
function remarkHighlight (line 3) | function remarkHighlight() {
FILE: src/lib/remark-plugins/imgRel.js
function remarkLogImages (line 4) | function remarkLogImages() {
FILE: src/lib/remark-plugins/obsidianImage.js
function obsidianImagePlugin (line 3) | function obsidianImagePlugin() {
FILE: src/lib/remark-plugins/remarkTags.ts
function remarkTags (line 3) | function remarkTags() {
FILE: src/lib/server/auth.ts
function getAuthenticatedPocketBase (line 10) | async function getAuthenticatedPocketBase() {
FILE: src/lib/utils.ts
function cn (line 6) | function cn(...inputs: ClassValue[]) {
type FlyAndScaleParams (line 10) | type FlyAndScaleParams = {
FILE: src/routes/+layout.server.ts
function load (line 13) | async function load({ fetch, params }) {
FILE: src/routes/+page.server.ts
function load (line 6) | async function load({ params }) {
function getBacklinks (line 38) | async function getBacklinks(url) {
FILE: src/routes/[...post].md/+page.server.ts
function getBacklinks (line 9) | async function getBacklinks(url) {
function computeGraphData (line 48) | async function computeGraphData(fileUrl) {
function load (line 84) | async function load({ params, fetch, locals }) {
FILE: src/routes/api/graph/+server.ts
function computeGraphData (line 24) | async function computeGraphData(currentPage) {
FILE: src/routes/api/img/[...path]/+server.ts
function getContentType (line 48) | function getContentType(filename: string): string {
FILE: src/routes/api/ls/+server.ts
type FileNode (line 5) | interface FileNode {
function buildFileTree (line 13) | function buildFileTree(records: any[]): FileNode[] {
FILE: src/routes/api/search/+server.ts
function extractSnippet (line 7) | function extractSnippet(content: string, query: string, snippetLength: n...
FILE: src/routes/api/upload/+server.ts
function verifyApiKey (line 31) | function verifyApiKey(request: Request): boolean {
function apiKeyMiddleware (line 39) | async function apiKeyMiddleware(
type Frontmatter (line 53) | interface Frontmatter {
type CompiledMD (line 58) | interface CompiledMD {
function customWikiLink (line 71) | function customWikiLink() {
function unescapeHtml (line 95) | function unescapeHtml(html: string): string {
function processContent (line 104) | function processContent(content: string): string {
function compileMarkdown (line 124) | async function compileMarkdown(fileContent: string, url: string): Promise {
function ensureMdbaseCollection (line 163) | async function ensureMdbaseCollection() {
function extractWikiLinks (line 310) | function extractWikiLinks(htmlContent: string): string[] {
function updateLinks (line 317) | async function updateLinks(recordId: string, content: string) {
function ensureAttachmentsCollection (line 366) | async function ensureAttachmentsCollection() {
function ensureTagsCollection (line 407) | async function ensureTagsCollection() {
type Tag (line 452) | interface Tag {
function parseTagsAndUpdatePocketBase (line 458) | async function parseTagsAndUpdatePocketBase(
FILE: src/writing/+page.server.ts
function load (line 6) | async function load({ params }) {
FILE: src/writing/[...post].md/+page.server.ts
function getBacklinks (line 9) | async function getBacklinks(url) {
function computeGraphData (line 43) | async function computeGraphData(fileUrl) {
function load (line 72) | async function load({ params, fetch, locals }) {
Condensed preview — 123 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (203K chars).
[
{
"path": ".dockerignore",
"chars": 3590,
"preview": "# flyctl launch added from .gitignore\n# Byte-compiled / optimized / DLL files\n**/__pycache__\n**/*.py[cod]\n**/*$py.class\n"
},
{
"path": ".gitignore",
"chars": 3141,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\nstdout\n\n# C extensions\n*.so\n\n# Distribution / "
},
{
"path": ".pre-commit-config.yaml",
"chars": 706,
"preview": "repos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n rev: v4.5.0\n hooks:\n - id: check-ast\n -"
},
{
"path": "Dockerfile",
"chars": 1251,
"preview": "FROM node:22.9.0-alpine3.20\n\nARG POCKETBASE_ADMIN_EMAIL\nARG POCKETBASE_ADMIN_PASSWORD\nARG POCKETBASE_URL\nARG TITLE\nARG A"
},
{
"path": "LICENSE",
"chars": 1082,
"preview": "MIT License\n\nCopyright (c) 2024 Rishikanth Chandrasekaran\n\nPermission is hereby granted, free of charge, to any person o"
},
{
"path": "README.md",
"chars": 6495,
"preview": "## Introduction\nHi,\nI’m [Rishikanth](https://rishikanth.me), and I’m excited to introduce you to Markopolis! It’s a web "
},
{
"path": "components.json",
"chars": 278,
"preview": "{\n\t\"$schema\": \"https://shadcn-svelte.com/schema.json\",\n\t\"style\": \"new-york\",\n\t\"tailwind\": {\n\t\t\"config\": \"tailwind.config"
},
{
"path": "docker-compose.yaml",
"chars": 442,
"preview": "services:\n markopolis:\n image: ghcr.io/rishikanthc/markopolis:3.0.0\n ports:\n - \"8080:8080\"\n - \"3000:300"
},
{
"path": "docs/Changelog/3.0.0.md",
"chars": 1012,
"preview": "---\ntitle: Version 3.0.0\ndate: 09-24-2024\ntags:\n - 3.0.0\n\n---\n\n## Backend Rewrite\n* **Technology Shift:** The backend h"
},
{
"path": "docs/Development/APIs.md",
"chars": 2495,
"preview": "---\ntitle: API overview\ndate: 22-09-2024\ntags:\n - api\n - backend\npublish: true\n---\n\nThis section details the core oper"
},
{
"path": "docs/Development/Markdown Rendering.md",
"chars": 2005,
"preview": "---\ntitle: Rendering Markdown as HTML\ndate: 09-22-2024\ntags:\n - markdown\n - mdsvex\npublish: true\n---\n\nThis document pr"
},
{
"path": "docs/Development/components.md",
"chars": 1488,
"preview": "---\ntitle: Components behind Markopolis\ntags:\n - development\n - working\ndate: 09/20/2024\npublish: true\n---\n\nMarkopolis"
},
{
"path": "docs/Markdown Syntax.md",
"chars": 5168,
"preview": "---\npublish: true\ntags:\n- syntax\n- markdown\ntitle: Markdown Syntax\n---\n# Headings\n\n```markdown\n# Heading 1\n## Heading 2\n"
},
{
"path": "docs/installation.md",
"chars": 2064,
"preview": "---\ntitle: Installation\ndate: 09-24-2024\ntags:\n - install\n - docker\n---\n\nInstalling Markopolis involves two steps. Fir"
},
{
"path": "docs/introduction.md",
"chars": 2494,
"preview": "---\ntitle: Hi,\ndate: 09-23-2024\n---\n\nWelcome to Markopolis, a self-hostable web app and API for managing Markdown knowle"
},
{
"path": "docs/roadmap.md",
"chars": 1359,
"preview": "---\ntitle: Development Roadmap\ndate: 09-23-2024\npublish: true\n---\n\nThis page lists a bunch of features and improvements "
},
{
"path": "docs/usage.md",
"chars": 830,
"preview": "---\ntitle: Usage\ndate: 09-24-2024\n---\n\nInitially the app starts with an empty database as there are no notes. So we\nwill"
},
{
"path": "eslint.config.js",
"chars": 649,
"preview": "import js from '@eslint/js';\nimport ts from 'typescript-eslint';\nimport svelte from 'eslint-plugin-svelte';\nimport prett"
},
{
"path": "package.json",
"chars": 2289,
"preview": "{\n\t\"name\": \"godamn\",\n\t\"version\": \"0.0.1\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"dev\": \"vite dev\",\n\t\t\"build\": \"vite build\",\n"
},
{
"path": "postcss.config.js",
"chars": 73,
"preview": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {}\n\t}\n};\n"
},
{
"path": "src/app.css",
"chars": 7192,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n\t:root {\n\t\t--background: 0 0% 100%;\n\t\t--foregr"
},
{
"path": "src/app.d.ts",
"chars": 331,
"preview": "// See https://kit.svelte.dev/docs/types#app\n// for information about these interfaces\ndeclare global {\n\tnamespace App {"
},
{
"path": "src/app.html",
"chars": 881,
"preview": "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"%sveltekit.ass"
},
{
"path": "src/lib/components/FileTree.svelte",
"chars": 1660,
"preview": "<script lang=\"ts\">\n\timport { onMount } from \"svelte\";\n\timport { ChevronRight, ChevronDown, File } from \"lucide-svelte\";\n"
},
{
"path": "src/lib/components/GraphScene.svelte",
"chars": 1485,
"preview": "<script lang=\"ts\">\n\timport { T } from '@threlte/core';\n\timport { Grid, OrbitControls, interactivity } from '@threlte/ext"
},
{
"path": "src/lib/components/LoginForm.svelte",
"chars": 1437,
"preview": "<script lang=\"ts\">\n\timport * as Form from '$lib/components/ui/form';\n\timport { Input } from '$lib/components/ui/input';\n"
},
{
"path": "src/lib/components/MDGraph.svelte",
"chars": 934,
"preview": "<script>\n\timport { onMount } from 'svelte';\n\timport { browser } from '$app/environment';\n\timport * as Card from '$lib/co"
},
{
"path": "src/lib/components/MDsvexRenderer.svelte",
"chars": 9109,
"preview": "<!-- MDsveXContentRenderer.svelte -->\n<script lang=\"ts\">\n\timport { onMount, tick } from \"svelte\";\n\timport { afterNavigat"
},
{
"path": "src/lib/components/MarkdownGraph.svelte",
"chars": 1148,
"preview": "<script>\n\timport { onMount } from 'svelte';\n\timport { Canvas } from '@threlte/core';\n\t/* import Scene from '$lib/compone"
},
{
"path": "src/lib/components/Scene.svelte",
"chars": 783,
"preview": "<script lang=\"ts\">\n\timport { T } from '@threlte/core';\n\timport { Grid, OrbitControls, interactivity } from '@threlte/ext"
},
{
"path": "src/lib/components/SearchComponent.svelte",
"chars": 4728,
"preview": "<script lang=\"ts\">\n\timport { onMount } from 'svelte';\n\timport { fade } from 'svelte/transition';\n\timport { debounce } fr"
},
{
"path": "src/lib/components/Sidebar.svelte",
"chars": 1137,
"preview": "<script lang=\"ts\">\n\timport { Sun, Moon, User } from 'lucide-svelte';\n\n\timport { toggleMode } from 'mode-watcher';\n\timpor"
},
{
"path": "src/lib/components/TagBar.svelte",
"chars": 966,
"preview": "<script lang=\"ts\">\n\timport { afterNavigate, beforeNavigate } from \"$app/navigation\";\n\timport { Tags } from \"lucide-svelt"
},
{
"path": "src/lib/components/TopBar.svelte",
"chars": 613,
"preview": "<script lang=\"ts\">\n\timport { Button } from '$lib/components/ui/button';\n\timport { Menu } from 'lucide-svelte';\n\timport {"
},
{
"path": "src/lib/components/award.svelte",
"chars": 2770,
"preview": "<script>\n\texport let starColor = '#000';\n\texport let size = 30;\n</script>\n\n<svg\n\twidth=\"{size}px\"\n\theight=\"{size}px\"\n\tve"
},
{
"path": "src/lib/components/schema.ts",
"chars": 157,
"preview": "import { z } from 'zod';\n\nexport const formSchema = z.object({\n\tusername: z.string(),\n\tpassword: z.string()\n});\n\nexport "
},
{
"path": "src/lib/components/ui/button/button.svelte",
"chars": 651,
"preview": "<script lang=\"ts\">\n\timport { Button as ButtonPrimitive } from \"bits-ui\";\n\timport { type Events, type Props, buttonVarian"
},
{
"path": "src/lib/components/ui/button/index.ts",
"chars": 1528,
"preview": "import type { Button as ButtonPrimitive } from \"bits-ui\";\nimport { type VariantProps, tv } from \"tailwind-variants\";\nimp"
},
{
"path": "src/lib/components/ui/card/card-content.svelte",
"chars": 325,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/card/card-description.svelte",
"chars": 348,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/card/card-footer.svelte",
"chars": 343,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/card/card-header.svelte",
"chars": 346,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/card/card-title.svelte",
"chars": 517,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { HeadingLevel } from \"./index.j"
},
{
"path": "src/lib/components/ui/card/card.svelte",
"chars": 497,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/card/index.ts",
"chars": 542,
"preview": "import Root from \"./card.svelte\";\nimport Content from \"./card-content.svelte\";\nimport Description from \"./card-descripti"
},
{
"path": "src/lib/components/ui/dialog/dialog-content.svelte",
"chars": 1267,
"preview": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport Cross2 from \"svelte-radix/Cross2.svelte"
},
{
"path": "src/lib/components/ui/dialog/dialog-description.svelte",
"chars": 397,
"preview": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$P"
},
{
"path": "src/lib/components/ui/dialog/dialog-footer.svelte",
"chars": 381,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/dialog/dialog-header.svelte",
"chars": 367,
"preview": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $"
},
{
"path": "src/lib/components/ui/dialog/dialog-overlay.svelte",
"chars": 587,
"preview": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { fade } from \"svelte/transition\";\n\timp"
},
{
"path": "src/lib/components/ui/dialog/dialog-portal.svelte",
"chars": 208,
"preview": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\n\ttype $$Props = DialogPrimitive.PortalProps;\n<"
},
{
"path": "src/lib/components/ui/dialog/dialog-title.svelte",
"chars": 399,
"preview": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$P"
},
{
"path": "src/lib/components/ui/dialog/index.ts",
"chars": 860,
"preview": "import { Dialog as DialogPrimitive } from \"bits-ui\";\n\nimport Title from \"./dialog-title.svelte\";\nimport Portal from \"./d"
},
{
"path": "src/lib/components/ui/form/form-button.svelte",
"chars": 246,
"preview": "<script lang=\"ts\">\n\timport * as Button from \"$lib/components/ui/button/index.js\";\n\n\ttype $$Props = Button.Props;\n\ttype $"
},
{
"path": "src/lib/components/ui/form/form-description.svelte",
"chars": 427,
"preview": "<script lang=\"ts\">\n\timport * as FormPrimitive from \"formsnap\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Props = For"
},
{
"path": "src/lib/components/ui/form/form-element-field.svelte",
"chars": 870,
"preview": "<script lang=\"ts\" context=\"module\">\n\timport type { FormPathLeaves, SuperForm } from \"sveltekit-superforms\";\n\ttype T = Re"
},
{
"path": "src/lib/components/ui/form/form-field-errors.svelte",
"chars": 698,
"preview": "<script lang=\"ts\">\n\timport * as FormPrimitive from \"formsnap\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Props = For"
},
{
"path": "src/lib/components/ui/form/form-field.svelte",
"chars": 828,
"preview": "<script lang=\"ts\" context=\"module\">\n\timport type { FormPath, SuperForm } from \"sveltekit-superforms\";\n\ttype T = Record<s"
},
{
"path": "src/lib/components/ui/form/form-fieldset.svelte",
"chars": 743,
"preview": "<script lang=\"ts\" context=\"module\">\n\timport type { FormPath, SuperForm } from \"sveltekit-superforms\";\n\ttype T = Record<s"
},
{
"path": "src/lib/components/ui/form/form-label.svelte",
"chars": 521,
"preview": "<script lang=\"ts\">\n\timport type { Label as LabelPrimitive } from \"bits-ui\";\n\timport { getFormControl } from \"formsnap\";\n"
},
{
"path": "src/lib/components/ui/form/form-legend.svelte",
"chars": 433,
"preview": "<script lang=\"ts\">\n\timport * as FormPrimitive from \"formsnap\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Props = For"
},
{
"path": "src/lib/components/ui/form/index.ts",
"chars": 817,
"preview": "import * as FormPrimitive from \"formsnap\";\nimport Description from \"./form-description.svelte\";\nimport Label from \"./for"
},
{
"path": "src/lib/components/ui/input/index.ts",
"chars": 830,
"preview": "import Root from \"./input.svelte\";\n\nexport type FormInputEvent<T extends Event = Event> = T & {\n\tcurrentTarget: EventTar"
},
{
"path": "src/lib/components/ui/input/input.svelte",
"chars": 1151,
"preview": "<script lang=\"ts\">\n\timport type { HTMLInputAttributes } from \"svelte/elements\";\n\timport type { InputEvents } from \"./ind"
},
{
"path": "src/lib/components/ui/label/index.ts",
"chars": 75,
"preview": "import Root from \"./label.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Label,\n};\n"
},
{
"path": "src/lib/components/ui/label/label.svelte",
"chars": 435,
"preview": "<script lang=\"ts\">\n\timport { Label as LabelPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Pro"
},
{
"path": "src/lib/components/ui/scroll-area/index.ts",
"chars": 190,
"preview": "import Scrollbar from \"./scroll-area-scrollbar.svelte\";\nimport Root from \"./scroll-area.svelte\";\n\nexport {\n\tRoot,\n\tScrol"
},
{
"path": "src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte",
"chars": 831,
"preview": "<script lang=\"ts\">\n\timport { ScrollArea as ScrollAreaPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\t"
},
{
"path": "src/lib/components/ui/scroll-area/scroll-area.svelte",
"chars": 1142,
"preview": "<script lang=\"ts\">\n\timport { ScrollArea as ScrollAreaPrimitive } from \"bits-ui\";\n\timport { Scrollbar } from \"./index.js\""
},
{
"path": "src/lib/components/ui/separator/index.ts",
"chars": 83,
"preview": "import Root from \"./separator.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Separator,\n};\n"
},
{
"path": "src/lib/components/ui/separator/separator.svelte",
"chars": 573,
"preview": "<script lang=\"ts\">\n\timport { Separator as SeparatorPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tty"
},
{
"path": "src/lib/highlightCode.ts",
"chars": 328,
"preview": "import { writable } from 'svelte/store';\nimport { mode } from 'mode-watcher';\n\nfunction createHighlightStore() {\n\tconst "
},
{
"path": "src/lib/index.ts",
"chars": 75,
"preview": "// place files you want to import through the `$lib` alias in this folder.\n"
},
{
"path": "src/lib/md.ts",
"chars": 1412,
"preview": "// src/lib/md.ts\n\nimport yaml from 'js-yaml';\n\n/**\n * Function to add or update frontmatter in Markdown content.\n *\n * @"
},
{
"path": "src/lib/pbStore.ts",
"chars": 800,
"preview": "import PocketBase from 'pocketbase';\nimport { writable } from 'svelte/store';\nimport { browser } from '$app/environment'"
},
{
"path": "src/lib/pocketbase.ts",
"chars": 1002,
"preview": "import PocketBase from 'pocketbase';\nimport {\n\tPOCKETBASE_URL,\n\tPOCKETBASE_ADMIN_EMAIL,\n\tPOCKETBASE_ADMIN_PASSWORD\n} fro"
},
{
"path": "src/lib/remark-plugins/footNotes.js",
"chars": 2080,
"preview": "// src/lib/plugins/remark-footnote-html.js\nimport { visit } from 'unist-util-visit';\n\n\nfunction remarkFootnoteHTML() {\n "
},
{
"path": "src/lib/remark-plugins/highlightSyn.js",
"chars": 1465,
"preview": "import { visit } from 'unist-util-visit'\n\nfunction remarkHighlight() {\n return (tree) => {\n visit(tree, 'text', (nod"
},
{
"path": "src/lib/remark-plugins/imgRel.js",
"chars": 838,
"preview": "import { visit } from 'unist-util-visit';\nimport path from 'path';\n\nexport default function remarkLogImages() {\n\treturn "
},
{
"path": "src/lib/remark-plugins/mermaidDiag.js",
"chars": 399,
"preview": "import { visit } from 'unist-util-visit';\n\n// Create the plugin\nconst remarkMermaid = () => {\n return (tree) => {\n v"
},
{
"path": "src/lib/remark-plugins/obsidianImage.js",
"chars": 1396,
"preview": "import { visit } from 'unist-util-visit';\n\nfunction obsidianImagePlugin() {\n return (tree) => {\n visit(tree, 'paragr"
},
{
"path": "src/lib/remark-plugins/remarkTags.ts",
"chars": 1523,
"preview": "import { visit } from 'unist-util-visit';\n\nfunction remarkTags() {\n\treturn (tree) => {\n\t\tvisit(tree, 'text', (node, inde"
},
{
"path": "src/lib/server/auth.ts",
"chars": 1424,
"preview": "import PocketBase from 'pocketbase';\nimport {\n\tPOCKETBASE_URL,\n\tPOCKETBASE_ADMIN_EMAIL,\n\tPOCKETBASE_ADMIN_PASSWORD\n} fro"
},
{
"path": "src/lib/stores/sidebarStore.ts",
"chars": 90,
"preview": "import { writable } from 'svelte/store';\n\nexport const isSidebarVisible = writable(true);\n"
},
{
"path": "src/lib/utils.ts",
"chars": 1589,
"preview": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\nimport { cubicOut } from \"svelte"
},
{
"path": "src/routes/+layout.server.ts",
"chars": 857,
"preview": "import { json } from \"@sveltejs/kit\";\nimport type { RequestHandler } from \"./$types\";\nimport { superValidate } from \"sve"
},
{
"path": "src/routes/+layout.svelte",
"chars": 2842,
"preview": "<script lang=\"ts\">\n\timport '../app.css';\n\timport { ModeWatcher } from 'mode-watcher';\n\timport { Separator } from '$lib/c"
},
{
"path": "src/routes/+page.server.ts",
"chars": 1882,
"preview": "import PocketBase from \"pocketbase\";\nimport { getAuthenticatedPocketBase } from \"$lib/server/auth\";\n\nconst pb = await ge"
},
{
"path": "src/routes/+page.svelte",
"chars": 1672,
"preview": "<script lang=\"ts\">\n\timport MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';\n\timport MDGraph from '$lib/compo"
},
{
"path": "src/routes/[...post].md/+page.server.ts",
"chars": 2965,
"preview": "import { json } from \"@sveltejs/kit\";\nimport type { RequestHandler } from \"./$types\";\nimport PocketBase from \"pocketbase"
},
{
"path": "src/routes/[...post].md/+page.svelte",
"chars": 1507,
"preview": "<script lang=\"ts\">\n\timport MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';\n\timport MDGraph from '$lib/compo"
},
{
"path": "src/routes/about/+page.svelte",
"chars": 0,
"preview": ""
},
{
"path": "src/routes/api/backlinks/+server.ts",
"chars": 1789,
"preview": "import type { RequestHandler } from '@sveltejs/kit';\nimport PocketBase from 'pocketbase';\nimport {\n\tPOCKETBASE_ADMIN_PAS"
},
{
"path": "src/routes/api/graph/+server.ts",
"chars": 1413,
"preview": "import { json } from '@sveltejs/kit';\nimport { getAuthenticatedPocketBase } from '$lib/server/auth';\nimport type { Reque"
},
{
"path": "src/routes/api/hello/+server.ts",
"chars": 588,
"preview": "// src/routes/api/hello/+server.ts\nimport type { RequestHandler } from './$types';\nimport { getAuthenticatedPocketBase }"
},
{
"path": "src/routes/api/img/[...path]/+server.ts",
"chars": 1776,
"preview": "import { error } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\nimport PocketBase from 'pocketbas"
},
{
"path": "src/routes/api/links/+server.ts",
"chars": 1929,
"preview": "import type { RequestHandler } from '@sveltejs/kit';\nimport PocketBase from 'pocketbase';\nimport {\n\tPOCKETBASE_ADMIN_PAS"
},
{
"path": "src/routes/api/ls/+server.ts",
"chars": 2190,
"preview": "import { json } from \"@sveltejs/kit\";\nimport type { RequestHandler } from \"./$types\";\nimport { getAuthenticatedPocketBas"
},
{
"path": "src/routes/api/search/+server.ts",
"chars": 1923,
"preview": "// api/search/+server.ts\nimport { json } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\nimport uF"
},
{
"path": "src/routes/api/tags/+server.ts",
"chars": 453,
"preview": "// src/routes/api/hello/+server.ts\nimport { json } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';"
},
{
"path": "src/routes/api/upload/+server.ts",
"chars": 24519,
"preview": "import type { RequestHandler } from \"@sveltejs/kit\";\nimport PocketBase from \"pocketbase\";\nimport path from \"path\";\nimpor"
},
{
"path": "src/routes/login/+page.server.ts",
"chars": 1871,
"preview": "import type { PageServerLoad, Actions } from './$types.js';\nimport { fail, redirect } from '@sveltejs/kit';\nimport { sup"
},
{
"path": "src/routes/login/+page.svelte",
"chars": 159,
"preview": "<script>\n\timport LoginForm from '$lib/components/LoginForm.svelte';\n\texport let data;\n\t$: formData = data?.form;\n</scrip"
},
{
"path": "src/routes/login/success/+page.server.ts",
"chars": 480,
"preview": "import type { PageServerLoad } from './$types';\nimport { redirect } from '@sveltejs/kit';\n\nexport const load: PageServer"
},
{
"path": "src/routes/login/success/+page.svelte",
"chars": 184,
"preview": "<!-- src/routes/dashboard/+page.svelte -->\n<script lang=\"ts\">\n\texport let data;\n</script>\n\n<h1>Welcome to your dashboard"
},
{
"path": "src/routes/publications/+page.svelte",
"chars": 4775,
"preview": "<script>\n\timport Award from '$lib/components/award.svelte';\n\texport const pubs = [\n\t\t{\n\t\t\ttitle:\n\t\t\t\t'A Neuro-Symbolic A"
},
{
"path": "src/routes/tags/[tag]/+page.server.ts",
"chars": 864,
"preview": "import PocketBase from 'pocketbase';\nimport { getAuthenticatedPocketBase } from '$lib/server/auth';\nimport type { PageSe"
},
{
"path": "src/routes/tags/[tag]/+page.svelte",
"chars": 373,
"preview": "<script lang=\"ts\">\n\texport let data;\n</script>\n\n<div class=\"p-2 text-6xl font-bold lg:pt-6\">\n\tTag: {data.tag}\n</div>\n\n<d"
},
{
"path": "src/writing/+page.server.ts",
"chars": 567,
"preview": "import PocketBase from 'pocketbase';\nimport { getAuthenticatedPocketBase } from '$lib/server/auth';\n\nconst pb = await ge"
},
{
"path": "src/writing/+page.svelte",
"chars": 754,
"preview": "<script>\n\texport let data;\n\n\tfunction formatDate(dateString) {\n\t\tconst date = new Date(dateString);\n\t\t// Format the date"
},
{
"path": "src/writing/[...dir]/+page.server.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/writing/[...dir]/+page.svelte",
"chars": 31,
"preview": "<div>This is a directory</div>\n"
},
{
"path": "src/writing/[...post].md/+page.server.ts",
"chars": 2611,
"preview": "import { json } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\nimport PocketBase from 'pocketbase"
},
{
"path": "src/writing/[...post].md/+page.svelte",
"chars": 638,
"preview": "<script lang=\"ts\">\n\timport MDsvexRenderer from '$lib/components/MDsvexRenderer.svelte';\n\timport MDGraph from '$lib/compo"
},
{
"path": "start.sh",
"chars": 729,
"preview": "#!/bin/sh\n\n# Ensure the environment variables are correctly set\necho \"Creating admin with email: ${POCKETBASE_ADMIN_EMAI"
},
{
"path": "start_services.sh",
"chars": 741,
"preview": "#!/bin/sh\n\n# Ensure the environment variables are correctly set\necho \"Creating admin with email: ${POCKETBASE_ADMIN_EMAI"
},
{
"path": "static/fonts.css",
"chars": 5205,
"preview": "@font-face {\n\tfont-family: 'IBM Plex Sans';\n\tsrc: url('/fonts/IBMPlexSans-Bold.ttf') format('truetype');\n\tfont-weight: b"
},
{
"path": "supervisord.conf",
"chars": 381,
"preview": "[supervisord]\nnodaemon=true\n\n[program:pocketbase]\ncommand=/pb/pocketbase serve --http=0.0.0.0:8090 --dev --dir /app/db\ns"
},
{
"path": "svelte.config.js",
"chars": 772,
"preview": "/* import adapter from '@sveltejs/adapter-auto'; */\nimport adapter from '@sveltejs/adapter-node';\n\nimport { vitePreproce"
},
{
"path": "tailwind.config.ts",
"chars": 2526,
"preview": "import { fontFamily } from 'tailwindcss/defaultTheme';\nimport type { Config } from 'tailwindcss';\n\nconst config: Config "
},
{
"path": "tsconfig.json",
"chars": 649,
"preview": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInte"
},
{
"path": "vite.config.ts",
"chars": 144,
"preview": "import { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n\tplu"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the rishikanthc/markopolis GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 123 files (177.6 KB), approximately 54.0k tokens, and a symbol index with 52 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.