mirror of
https://codeberg.org/Freedium-cfd/web.git
synced 2026-03-11 09:04:37 +00:00
refactor(new-freedium): connecting frontend with backend, integrating mongodb, fixing bugs and problems
This commit is contained in:
parent
6ad835d89f
commit
286691ea87
40 changed files with 1080 additions and 363 deletions
|
|
@ -5,7 +5,7 @@
|
|||
groups = ["default", "api", "dev"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:0503cfae0ec00418ac476d9055e2f0930705f732b82759dbee8e88ace7c7eeab"
|
||||
content_hash = "sha256:c8d5376c4072286f4b254f030eb7bf2e315723b69831378eea9d454568df6bad"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = "==3.12.*"
|
||||
|
|
@ -148,7 +148,7 @@ name = "dnspython"
|
|||
version = "2.7.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "DNS toolkit"
|
||||
groups = ["api"]
|
||||
groups = ["default", "api"]
|
||||
files = [
|
||||
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
|
||||
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
|
||||
|
|
@ -503,6 +503,20 @@ files = [
|
|||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "motor"
|
||||
version = "3.6.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Non-blocking MongoDB driver for Tornado or asyncio"
|
||||
groups = ["default"]
|
||||
dependencies = [
|
||||
"pymongo<4.10,>=4.9",
|
||||
]
|
||||
files = [
|
||||
{file = "motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894"},
|
||||
{file = "motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msgspec"
|
||||
version = "0.18.6"
|
||||
|
|
@ -743,6 +757,28 @@ files = [
|
|||
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymongo"
|
||||
version = "4.9.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Python driver for MongoDB <http://www.mongodb.org>"
|
||||
groups = ["default"]
|
||||
dependencies = [
|
||||
"dnspython<3.0.0,>=1.16.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de"},
|
||||
{file = "pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68"},
|
||||
{file = "pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ dependencies = [
|
|||
"pendulum>=3.0.0",
|
||||
"ujson>=5.10.0",
|
||||
"httpx[socks]>=0.27.2",
|
||||
"asyncclick>=8.1.7.2",
|
||||
"pymongo>=4.9.2",
|
||||
"motor>=3.6.0",
|
||||
]
|
||||
requires-python = "==3.12.*"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
__NAME__ = "Freedium Library"
|
||||
__VERSION__ = "0.0.1"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from freedium_library.__init__ import __VERSION__
|
||||
|
||||
__NAME__ = "Freedium Library API"
|
||||
|
||||
__all__ = ["__NAME__", "__VERSION__"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from fastapi import FastAPI
|
||||
from loguru import logger
|
||||
|
||||
from freedium_library.api.container import APIContainer
|
||||
from freedium_library.api.error import register_error_handler
|
||||
|
|
@ -7,18 +8,21 @@ from freedium_library.api.lifespan import lifespan
|
|||
from freedium_library.api.middlewares import register_middlewares
|
||||
from freedium_library.api.settings import ApplicationSettings
|
||||
|
||||
container = APIContainer()
|
||||
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
settings = ApplicationSettings()
|
||||
container = APIContainer()
|
||||
container.wire(modules=["freedium_library.api.settings"])
|
||||
|
||||
if container.config.DISABLE_DOCS:
|
||||
settings = ApplicationSettings(container=container)
|
||||
config = container.config()
|
||||
|
||||
if config.DISABLED_DOCS:
|
||||
logger.warning(f"Documentation is disabled: {config.DISABLED_DOCS}")
|
||||
settings.disable_docs()
|
||||
|
||||
app = FastAPI(**settings.to_dict(), lifespan=lifespan) # type: ignore
|
||||
|
||||
register_router(app, container.config.PREFIX_PATH)
|
||||
register_router(app, settings.prefix_path)
|
||||
register_error_handler(app)
|
||||
register_middlewares(app)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from pydantic import Field
|
||||
|
||||
from freedium_library.utils.meta.pydantic import BaseConfig, BaseSettingsConfigDict
|
||||
|
||||
|
||||
class APIConfig(BaseConfig):
|
||||
model_config = BaseSettingsConfigDict(env_prefix="API_")
|
||||
|
||||
DISABLE_DOCS: bool = True
|
||||
PREFIX_PATH: str = "/api"
|
||||
DISABLED_DOCS: bool = Field(default=False)
|
||||
PREFIX_PATH: str = Field(default="/api")
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ from freedium_library.api.config import APIConfig
|
|||
|
||||
|
||||
class APIContainer(containers.DeclarativeContainer):
|
||||
config: APIConfig = providers.Singleton(APIConfig) # type: ignore
|
||||
config = providers.Singleton(APIConfig)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,166 @@
|
|||
from functools import partial
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
handler_router = APIRouter()
|
||||
TEXT = """# V7 Is Here! 🚀
|
||||
|
||||
> "The best file upload solution just got even better!" - _Tech Weekly_
|
||||
|
||||
This release has been an absurd amount of work. So proud of the team and what we've built. Huge thanks to [Julius](https://github.com/julius) and [Mark](https://github.com/mark) for making this happen.
|
||||
|
||||
---
|
||||
|
||||
It is so, so hard to not go straight into the nerdy details, but the whole point of UploadThing is that you don't need to know ANY of those details. With that in mind, here's what's relevant for most y'all:
|
||||
|
||||
- UploadThing is now _way_ faster
|
||||
- Uploads can be **paused** and _resumed_ seamlessly
|
||||
- ~~Old limitations removed~~
|
||||
- More details...
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Feature | V6 | V7 |
|
||||
| ------------------ | ------ | --------- |
|
||||
| Upload Speed | 10MB/s | 50MB/s |
|
||||
| Concurrent Uploads | 100 | Unlimited |
|
||||
| Max File Size | 2GB | 10GB |
|
||||
|
||||
## Revolutionary Features
|
||||
|
||||
We've completely overhauled our backend infrastructure to bring you unparalleled performance. Our new distributed processing system can handle millions of concurrent uploads without breaking a sweat.
|
||||
|
||||
### Code Example
|
||||
|
||||
```typescript
|
||||
import { createUploadthing } from 'uploadthing/next';
|
||||
|
||||
const f = createUploadthing();
|
||||
|
||||
export const ourFileRouter = {
|
||||
imageUploader: f({ image: { maxFileSize: '4MB' } })
|
||||
.middleware(async () => {
|
||||
return { userId: 1234 };
|
||||
})
|
||||
.onUploadComplete(async ({ metadata, file }) => {
|
||||
console.log('Upload complete for userId:', metadata.userId);
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
### AI-Powered Optimization
|
||||
|
||||
UploadThing now leverages cutting-edge machine learning algorithms to optimize your uploads in real-time.
|
||||
|
||||
<details>
|
||||
<summary>Technical Details</summary>
|
||||
|
||||
- Uses TensorFlow.js for client-side optimizations
|
||||
- Implements WebAssembly for performance
|
||||
- Leverages Web Workers for background processing
|
||||
|
||||
</details>
|
||||
|
||||
## More Formatting Examples
|
||||
|
||||
### Advanced Markdown
|
||||
|
||||
#### Lists and Sublists
|
||||
|
||||
- Main Feature
|
||||
- Sub-feature 1
|
||||
- Sub-feature 2
|
||||
- Detailed point
|
||||
- Another detail
|
||||
- Another Feature
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step
|
||||
|
||||
#### Text Formatting
|
||||
|
||||
_Italic text_ and _another italic_
|
||||
**Bold text** and **another bold**
|
||||
**_Bold and italic_** **_together_**
|
||||
~~Strikethrough text~~
|
||||
`inline code`
|
||||
|
||||
### HTML Elements
|
||||
|
||||
<div align="center">
|
||||
<h3>Centered Content</h3>
|
||||
<p>This content is centered using `HTML`</p>
|
||||
</div>
|
||||
|
||||
<kbd>Ctrl</kbd> + <kbd>C</kbd> to copy
|
||||
|
||||
<mark>Highlighted text</mark>
|
||||
|
||||
<blockquote>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</blockquote>
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
This is hidden content that can be expanded!
|
||||
|
||||
<ul>
|
||||
<li>Point 1</li>
|
||||
<li>Point 2</li>
|
||||
<li>Point 3</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<aside>
|
||||
💡 Pro tip: You can combine <code>HTML</code> and <code>Markdown</code> for powerful formatting
|
||||
</aside>
|
||||
|
||||
### Custom Styling
|
||||
|
||||
<div style="background-color: #f0f0f0; padding: 1rem; border-radius: 4px;">
|
||||
<p style="color: #333;">Custom styled content box</p>
|
||||
<ul>
|
||||
<li>Feature highlight</li>
|
||||
<li>Important note</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
_For more information, visit our [documentation](https://docs.uploadthing.com)._
|
||||
|
||||
```js
|
||||
console.log('Hello, world!');
|
||||
```
|
||||
|
||||
```js
|
||||
console.log('Hello, world!');
|
||||
console.log('Hello, world!');
|
||||
console.log('Hello, world!');
|
||||
```
|
||||
|
||||
[](https://miro.medium.com/v2/resize:fit:320/1*abc123.jpg)
|
||||
> **Why React Hooks are the Future of React Development**
|
||||
> Learn how React Hooks are revolutionizing the way we write components and manage state in React applications. Discover the benefits and best practices.
|
||||
"""
|
||||
|
||||
|
||||
@handler_router.get("/render")
|
||||
async def render_page():
|
||||
return "render"
|
||||
async def render_page(service_name: str):
|
||||
return JSONResponse(content={"text": TEXT, "service_name": service_name})
|
||||
|
||||
|
||||
def register_render_router(router: APIRouter) -> None:
|
||||
router.add_api_route(path="/render", endpoint=render_page, methods=["GET"])
|
||||
render_router = APIRouter(prefix="/services")
|
||||
|
||||
for service_name in ["medium", "twitter"]:
|
||||
render_router.add_api_route(
|
||||
path=f"/{service_name}/render",
|
||||
endpoint=partial(render_page, service_name),
|
||||
summary=f"Render {service_name} page",
|
||||
description=f"Render {service_name} page",
|
||||
methods=["GET", "HEAD"],
|
||||
)
|
||||
|
||||
router.include_router(render_router)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
import uvicorn
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def start_server(host: str = "0.0.0.0", port: int = 7080) -> None:
|
||||
uvicorn.run("freedium_library.api.app:app", host=host, port=port)
|
||||
def start_server(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 7080,
|
||||
reload: bool = False,
|
||||
workers: Optional[int] = None,
|
||||
) -> None:
|
||||
if workers is None:
|
||||
workers = (os.cpu_count() or 1) + 1
|
||||
logger.warning(f"No workers specified, using CPU count {workers}")
|
||||
|
||||
uvicorn.run(
|
||||
"freedium_library.api.app:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
workers=workers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
|
||||
def register_middlewares(app: FastAPI) -> None:
|
||||
pass
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,41 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from freedium_library import __NAME__, __VERSION__
|
||||
from freedium_library.api.config import APIConfig
|
||||
from freedium_library.api.container import APIContainer
|
||||
|
||||
container = APIContainer()
|
||||
|
||||
class ApplicationSettings(BaseModel):
|
||||
title: str = Field(default=f"{__NAME__} API Service")
|
||||
version: str = Field(default=__VERSION__)
|
||||
prefix_path: str = Field(default="/api")
|
||||
openapi_url: Optional[str] = None
|
||||
docs_url: Optional[str] = None
|
||||
redoc_url: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class ApplicationSettings:
|
||||
title: str = __NAME__
|
||||
version: str = __VERSION__
|
||||
openapi_url: str | None = f"{container.config.PREFIX_PATH}/openapi.json"
|
||||
docs_url: str | None = f"{container.config.PREFIX_PATH}/docs"
|
||||
redoc_url: str | None = f"{container.config.PREFIX_PATH}/redoc"
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
@inject
|
||||
def __init__(
|
||||
self,
|
||||
title: str = __NAME__,
|
||||
version: str = __VERSION__,
|
||||
api_config: APIConfig = Provide[APIContainer.config],
|
||||
**data: Any,
|
||||
):
|
||||
prefix_path = api_config.PREFIX_PATH
|
||||
super().__init__(
|
||||
title=title,
|
||||
version=version,
|
||||
prefix_path=prefix_path,
|
||||
openapi_url=f"{prefix_path}/openapi.json",
|
||||
docs_url=f"{prefix_path}/docs",
|
||||
redoc_url=f"{prefix_path}/redoc",
|
||||
**data,
|
||||
)
|
||||
|
||||
def disable_docs(self) -> None:
|
||||
self.openapi_url = None
|
||||
|
|
@ -20,10 +43,4 @@ class ApplicationSettings:
|
|||
self.redoc_url = None
|
||||
|
||||
def to_dict(self) -> dict[str, str | None]:
|
||||
return {
|
||||
"title": self.title,
|
||||
"version": self.version,
|
||||
"openapi_url": self.openapi_url,
|
||||
"docs_url": self.docs_url,
|
||||
"redoc_url": self.redoc_url,
|
||||
}
|
||||
return self.model_dump()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import asyncclick as click
|
||||
import click
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--host", default="0.0.0.0", help="Host address to bind to")
|
||||
@click.option("--port", default=7080, type=int, help="Port to listen on")
|
||||
async def cli(host: str, port: int):
|
||||
@click.option(
|
||||
"--hot-reload",
|
||||
is_flag=True,
|
||||
help="Reload the server on code changes",
|
||||
default=False,
|
||||
)
|
||||
def cli(host: str, port: int, hot_reload: bool):
|
||||
from freedium_library.api.main import start_server
|
||||
|
||||
start_server(host=host, port=port)
|
||||
start_server(host=host, port=port, reload=hot_reload)
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ class MediumApiService:
|
|||
"Connection": "Keep-Alive",
|
||||
}
|
||||
|
||||
if self.auth_cookies is not None:
|
||||
headers["Cookie"] = self.auth_cookies
|
||||
if self.config.cookies is not None:
|
||||
headers["Cookie"] = self.config.cookies
|
||||
|
||||
graphql_data = {
|
||||
"operationName": "FullPostQuery",
|
||||
|
|
|
|||
0
freedium-library/src/freedium_library/utils/cache/db.py
vendored
Normal file
0
freedium-library/src/freedium_library/utils/cache/db.py
vendored
Normal file
0
freedium-library/src/freedium_library/utils/cache/db/__init__.py
vendored
Normal file
0
freedium-library/src/freedium_library/utils/cache/db/__init__.py
vendored
Normal file
103
freedium-library/src/freedium_library/utils/cache/db/base.py
vendored
Normal file
103
freedium-library/src/freedium_library/utils/cache/db/base.py
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List, Union
|
||||
|
||||
from ..models import CacheResponse
|
||||
|
||||
|
||||
class AbstractCacheBackend(ABC):
|
||||
@abstractmethod
|
||||
def connect(self) -> None:
|
||||
"""Initialize database connection"""
|
||||
raise NotImplementedError("Connect must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def ensure_connection(self) -> None:
|
||||
"""Ensure database connection is established"""
|
||||
raise NotImplementedError("Ensure_connection must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def aensure_connection(self) -> None:
|
||||
"""Ensure database connection is established asynchronously"""
|
||||
raise NotImplementedError(
|
||||
"Async ensure_connection must be implemented by subclass"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def init_db(self) -> None:
|
||||
"""Initialize database schema"""
|
||||
raise NotImplementedError("Init_db must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def ainit_db(self) -> None:
|
||||
"""Initialize database schema asynchronously"""
|
||||
raise NotImplementedError("Async init_db must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def all(self) -> List[dict]:
|
||||
"""Retrieve all cached items"""
|
||||
raise NotImplementedError("All must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def aall(self) -> List[dict]:
|
||||
"""Retrieve all cached items asynchronously"""
|
||||
raise NotImplementedError("Async all must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def all_length(self) -> int:
|
||||
"""Get total number of cached items"""
|
||||
raise NotImplementedError("All_length must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def aall_length(self) -> int:
|
||||
"""Get total number of cached items asynchronously"""
|
||||
raise NotImplementedError("Async all_length must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def random(self, size: int) -> List[CacheResponse]:
|
||||
"""Get random cached items"""
|
||||
raise NotImplementedError("Random must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def arandom(self, size: int) -> List[CacheResponse]:
|
||||
"""Get random cached items asynchronously"""
|
||||
raise NotImplementedError("Async random must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def pull(self, key: str) -> Union[CacheResponse, None]:
|
||||
"""Retrieve item from cache"""
|
||||
raise NotImplementedError("Pull must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def apull(self, key: str) -> Union[CacheResponse, None]:
|
||||
"""Retrieve item from cache asynchronously"""
|
||||
raise NotImplementedError("Async pull must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def push(self, key: str, value: Union[str, dict]) -> None:
|
||||
"""Store item in cache"""
|
||||
raise NotImplementedError("Push must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def apush(self, key: str, value: Union[str, dict]) -> None:
|
||||
"""Store item in cache asynchronously"""
|
||||
raise NotImplementedError("Async push must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, key: str) -> None:
|
||||
"""Remove item from cache"""
|
||||
raise NotImplementedError("Delete must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def adelete(self, key: str) -> None:
|
||||
"""Remove item from cache asynchronously"""
|
||||
raise NotImplementedError("Async delete must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""Close database connection"""
|
||||
raise NotImplementedError("Close must be implemented by subclass")
|
||||
|
||||
@abstractmethod
|
||||
async def aclose(self) -> None:
|
||||
"""Close database connection asynchronously"""
|
||||
raise NotImplementedError("Async close must be implemented by subclass")
|
||||
139
freedium-library/src/freedium_library/utils/cache/db/mongo.py
vendored
Normal file
139
freedium-library/src/freedium_library/utils/cache/db/mongo.py
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from typing import Union
|
||||
|
||||
import pymongo
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
from freedium_library.utils.json import json
|
||||
|
||||
from ..models import CacheResponse
|
||||
from .base import AbstractCacheBackend
|
||||
|
||||
|
||||
class MongoDBCacheBackend(AbstractCacheBackend):
|
||||
def __init__(
|
||||
self,
|
||||
connection_string: str,
|
||||
database: str = "freedium_cache",
|
||||
collection: str = "cache",
|
||||
):
|
||||
self.connection_string = connection_string
|
||||
self.database_name = database
|
||||
self.collection_name = collection
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.collection = None
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self.client = pymongo.MongoClient(self.connection_string)
|
||||
self.db = self.client[self.database_name]
|
||||
self.collection = self.db[self.collection_name]
|
||||
|
||||
def ensure_connection(self):
|
||||
if self.client is None:
|
||||
self.connect()
|
||||
|
||||
def init_db(self):
|
||||
self.ensure_connection()
|
||||
self.collection.create_index("key", unique=True)
|
||||
|
||||
def all(self):
|
||||
self.ensure_connection()
|
||||
return list(self.collection.find())
|
||||
|
||||
def all_length(self) -> int:
|
||||
self.ensure_connection()
|
||||
return self.collection.count_documents({})
|
||||
|
||||
def random(self, size: int) -> list[CacheResponse]:
|
||||
self.ensure_connection()
|
||||
pipeline = [{"$sample": {"size": size}}]
|
||||
results = self.collection.aggregate(pipeline)
|
||||
return [CacheResponse(doc["key"], doc["value"]) for doc in results]
|
||||
|
||||
def pull(self, key: str) -> Union[CacheResponse, None]:
|
||||
self.ensure_connection()
|
||||
doc = self.collection.find_one({"key": key})
|
||||
if doc:
|
||||
logger.debug("Value found in DB, returning it")
|
||||
return CacheResponse(key, doc["value"])
|
||||
logger.debug(f"No value found for key: {key}")
|
||||
return None
|
||||
|
||||
def push(self, key: str, value: Union[str, dict]) -> None:
|
||||
if isinstance(value, dict):
|
||||
value = json.dumps(value)
|
||||
elif not isinstance(value, str):
|
||||
raise ValueError(
|
||||
f"value argument should be a string or dict, not {type(value).__name__}"
|
||||
)
|
||||
|
||||
self.ensure_connection()
|
||||
self.collection.update_one(
|
||||
{"key": key}, {"$set": {"key": key, "value": value}}, upsert=True
|
||||
)
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
self.ensure_connection()
|
||||
result = self.collection.delete_one({"key": key})
|
||||
if result.deleted_count > 0:
|
||||
logger.debug(f"Deleted key: {key}")
|
||||
else:
|
||||
logger.warning(f"Attempted to delete non-existing key: {key}")
|
||||
|
||||
def close(self):
|
||||
if self.client:
|
||||
self.client.close()
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.collection = None
|
||||
|
||||
|
||||
class AsyncMongoDBCacheBackend(AbstractCacheBackend):
|
||||
def __init__(
|
||||
self,
|
||||
connection_string: str,
|
||||
database: str = "cache_db",
|
||||
collection: str = "cache",
|
||||
):
|
||||
self.connection_string = connection_string
|
||||
self.database_name = database
|
||||
self.collection_name = collection
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.collection = None
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self.client = AsyncIOMotorClient(self.connection_string)
|
||||
self.db = self.client[self.database_name]
|
||||
self.collection = self.db[self.collection_name]
|
||||
|
||||
async def aensure_connection(self):
|
||||
if self.client is None:
|
||||
self.connect()
|
||||
|
||||
async def ainit_db(self):
|
||||
await self.aensure_connection()
|
||||
await self.collection.create_index("key", unique=True)
|
||||
|
||||
async def aall(self):
|
||||
await self.aensure_connection()
|
||||
return await self.collection.find().to_list(None)
|
||||
|
||||
async def aall_length(self) -> int:
|
||||
await self.aensure_connection()
|
||||
return await self.collection.count_documents({})
|
||||
|
||||
async def arandom(self, size: int) -> list[CacheResponse]:
|
||||
await self.aensure_connection()
|
||||
pipeline = [{"$sample": {"size": size}}]
|
||||
results = await self.collection.aggregate(pipeline).to_list(None)
|
||||
return [CacheResponse(doc["key"], doc["value"]) for doc in results]
|
||||
|
||||
async def aclose(self):
|
||||
if self.client:
|
||||
self.client.close()
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.collection = None
|
||||
|
|
@ -128,3 +128,4 @@ class JSON:
|
|||
|
||||
|
||||
json_instance = JSON()
|
||||
json = json_instance
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from functools import partial
|
||||
from typing import cast
|
||||
from typing import Type, cast
|
||||
|
||||
from pydantic_settings import BaseSettings as _BaseSettings
|
||||
from pydantic_settings import SettingsConfigDict as _SettingsConfigDict
|
||||
|
|
@ -8,8 +8,8 @@ from pydantic_settings import SettingsConfigDict as _SettingsConfigDict
|
|||
class BaseConfig(_BaseSettings): ...
|
||||
|
||||
|
||||
BaseSettingsConfigDict: _SettingsConfigDict = cast(
|
||||
_SettingsConfigDict,
|
||||
BaseSettingsConfigDict: Type[_SettingsConfigDict] = cast(
|
||||
Type[_SettingsConfigDict],
|
||||
partial(
|
||||
_SettingsConfigDict,
|
||||
env_file=".env",
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
# source: https://github.com/thorio/KGrabber/issues/35#issuecomment-1164056688
|
||||
# curl -L --user-agent "Mozilla/5.0" --cipher AES256-SHA256 --tls-max 1.2 "<url>"
|
||||
|
||||
import sys
|
||||
|
||||
async def main():
|
||||
# Create a default SSL context
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# Disable TLS versions older than 1.2
|
||||
ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_3 # ??? - --tls-max
|
||||
|
||||
# Set cipher suite
|
||||
ssl_context.set_ciphers('AES256-SHA')
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0'}
|
||||
|
||||
# Configure aiohttp ClientSession with custom SSL context and User-Agent
|
||||
async with aiohttp.ClientSession(headers=headers) as session:
|
||||
async with session.get('https://github.zhaoqumao.com/veggiemonk/awesome-docker', ssl=ssl_context) as response:
|
||||
print(await response.text())
|
||||
print(response.status)
|
||||
|
||||
|
||||
###
|
||||
|
||||
# Port to Python 3.10+
|
||||
# https://github.com/fedora-infra/fedora-messaging/pull/250/commits/f3439350de7f55d42f4350e662392e9ffbdde028
|
||||
|
||||
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) # ??? - purpose
|
||||
if sys.version_info >= (3, 7):
|
||||
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
else:
|
||||
ssl_context.options |= ssl.OP_NO_SSLv2
|
||||
ssl_context.options |= ssl.OP_NO_SSLv3
|
||||
ssl_context.options |= ssl.OP_NO_TLSv1
|
||||
ssl_context.options |= ssl.OP_NO_TLSv1_1
|
||||
Binary file not shown.
|
|
@ -18,6 +18,7 @@
|
|||
"@iconify-json/mdi": "^1.2.1",
|
||||
"@iconify-json/simple-icons": "^1.2.12",
|
||||
"@iconify-json/teenyicons": "^1.2.1",
|
||||
"@shikijs/rehype": "^1.24.2",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/kit": "^2.7.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
"hast-util-to-html": "^9.0.3",
|
||||
"mdsvex": "^0.12.3",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"shiki": "^1.23.1",
|
||||
"shiki-transformer-copy-button": "^0.0.3",
|
||||
|
|
|
|||
8
new-web/src/api.js
Normal file
8
new-web/src/api.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { ofetch } from "ofetch";
|
||||
import config from "./config";
|
||||
|
||||
const apiFetch = ofetch.create({
|
||||
baseURL: config.API_URL,
|
||||
});
|
||||
|
||||
export default apiFetch;
|
||||
7
new-web/src/config.js
Normal file
7
new-web/src/config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { env } from "$env/dynamic/public";
|
||||
|
||||
const API_URL = env.VITE_API_URL || "http://localhost:7080/api";
|
||||
|
||||
export default {
|
||||
API_URL,
|
||||
};
|
||||
32
new-web/src/lib/codeCopy.js
Normal file
32
new-web/src/lib/codeCopy.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export function initializeCodeCopyButtons() {
|
||||
document.querySelectorAll(".code-copy-btn").forEach((button) => {
|
||||
button.addEventListener("click", handleCodeCopy);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCodeCopy(event) {
|
||||
const button = event.currentTarget;
|
||||
if (button.classList.contains("copied")) return;
|
||||
|
||||
const code = button.dataset.code;
|
||||
const toggleMs = parseInt(button.dataset.toggleMs, 10);
|
||||
const ready = button.querySelector(".ready");
|
||||
const success = button.querySelector(".success");
|
||||
|
||||
navigator.clipboard.writeText(code);
|
||||
button.classList.add("copied");
|
||||
ready.style.display = "none";
|
||||
success.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove("copied");
|
||||
ready.style.display = "block";
|
||||
success.style.display = "none";
|
||||
}, toggleMs);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("toast", {
|
||||
detail: { message: "Copied to clipboard" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
207
new-web/src/lib/elements/ArticlePage.css
Normal file
207
new-web/src/lib/elements/ArticlePage.css
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/* Headings */
|
||||
.prose h1 {
|
||||
@apply mt-12 mb-6 text-4xl font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
@apply mt-12 mb-4 text-3xl font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
@apply mt-8 mb-3 text-2xl font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
@apply mt-6 mb-2 text-xl font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose h5 {
|
||||
@apply mt-4 mb-2 text-lg font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose h6 {
|
||||
@apply mt-4 mb-2 text-base font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
/* Paragraphs and spacing */
|
||||
.prose p {
|
||||
@apply mt-4 leading-7 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.prose > *:first-child {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.prose ul {
|
||||
@apply mt-4 ml-6 space-y-2 list-disc list-outside;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
@apply mt-4 ml-6 space-y-2 list-decimal list-outside;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
@apply leading-7 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
.prose blockquote {
|
||||
@apply pl-4 my-6 italic border-l-4 border-gray-300 dark:border-gray-700;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.prose pre {
|
||||
@apply p-4 my-6 overflow-x-auto rounded-lg;
|
||||
}
|
||||
|
||||
.prose code:not(.shiki code) {
|
||||
@apply px-1.5 py-0.5 rounded text-sm font-mono bg-gray-200 dark:bg-zinc-700;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
@apply p-0 text-sm leading-relaxed bg-transparent;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.prose table {
|
||||
@apply w-full my-6 border-collapse;
|
||||
}
|
||||
|
||||
.prose th {
|
||||
@apply px-4 py-2 text-left bg-gray-100 border border-gray-300 dark:border-gray-700 dark:bg-zinc-800;
|
||||
}
|
||||
|
||||
.prose td {
|
||||
@apply px-4 py-2 border border-gray-300 dark:border-gray-700;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.prose a {
|
||||
@apply underline text-primary hover:text-primary/90 decoration-primary/30 hover:decoration-primary/50;
|
||||
}
|
||||
|
||||
.prose a::after {
|
||||
content: "";
|
||||
@apply icon-[heroicons-outline--external-link] inline-block ml-1 w-3.5 h-3.5 align-text-bottom;
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
.prose hr {
|
||||
@apply my-8 border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
.prose img {
|
||||
@apply mx-auto my-6 rounded-lg;
|
||||
}
|
||||
|
||||
/* Inline elements */
|
||||
.prose strong {
|
||||
@apply font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.prose del {
|
||||
@apply line-through;
|
||||
}
|
||||
|
||||
/* Definition lists */
|
||||
.prose dl {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
.prose dt {
|
||||
@apply font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose dd {
|
||||
@apply mt-2 ml-4;
|
||||
}
|
||||
|
||||
/* Nested lists spacing */
|
||||
.prose li > ul,
|
||||
.prose li > ol {
|
||||
@apply mt-2 mb-2;
|
||||
}
|
||||
|
||||
/* Code block filename */
|
||||
.prose .filename {
|
||||
@apply mt-6 -mb-2 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* Summary blocks */
|
||||
.prose summary {
|
||||
@apply leading-7 text-gray-700 cursor-pointer select-none dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100;
|
||||
}
|
||||
|
||||
.prose details {
|
||||
@apply p-4 my-4 rounded-lg bg-gray-50 dark:bg-zinc-800;
|
||||
}
|
||||
|
||||
.prose details[open] summary {
|
||||
@apply mb-3;
|
||||
}
|
||||
|
||||
/* Aside blocks */
|
||||
.prose aside {
|
||||
@apply p-4 my-6 border-l-4 rounded-lg border-primary bg-primary/5;
|
||||
}
|
||||
|
||||
.prose aside p {
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
/* Mark blocks */
|
||||
.prose mark {
|
||||
@apply bg-primary/20 text-primary px-1.5 py-0.5 rounded;
|
||||
}
|
||||
|
||||
/* Keyboard blocks */
|
||||
.prose kbd {
|
||||
@apply px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500;
|
||||
}
|
||||
|
||||
/* Mixtape embeds */
|
||||
.prose .mixtape-embed {
|
||||
@apply items-center p-2 overflow-hidden border border-gray-300 mt-7;
|
||||
}
|
||||
|
||||
.prose .mixtape-content {
|
||||
@apply flex flex-row justify-between p-2 overflow-hidden;
|
||||
}
|
||||
|
||||
.prose .mixtape-text {
|
||||
@apply flex flex-col justify-center p-2;
|
||||
}
|
||||
|
||||
.prose .mixtape-text h2 {
|
||||
@apply text-base font-bold text-black dark:text-gray-100;
|
||||
}
|
||||
|
||||
.prose .mixtape-description {
|
||||
@apply block mt-2;
|
||||
}
|
||||
|
||||
.prose .mixtape-description h3 {
|
||||
@apply text-sm text-gray-700;
|
||||
}
|
||||
|
||||
.prose .mixtape-site {
|
||||
@apply mt-5;
|
||||
}
|
||||
|
||||
.prose .mixtape-site p {
|
||||
@apply text-xs text-gray-700;
|
||||
}
|
||||
|
||||
.prose .mixtape-image {
|
||||
@apply relative flex flex-row h-40 w-60;
|
||||
}
|
||||
|
||||
.prose .mixtape-image-inner {
|
||||
@apply absolute inset-0 bg-center bg-cover;
|
||||
}
|
||||
|
|
@ -1,45 +1,19 @@
|
|||
<script>
|
||||
import Header from '$lib/elements/Header.svelte';
|
||||
import { formatDate } from '$lib/utils/dateFormatter';
|
||||
import { onMount } from 'svelte';
|
||||
import ImageZoom from '$lib/elements/ImageZoom.svelte';
|
||||
import Skeleton from '$lib/components/ui/skeleton/skeleton.svelte';
|
||||
import Footer from '$lib/elements/Footer.svelte';
|
||||
import './ArticlePage.css';
|
||||
|
||||
let data = {
|
||||
title: 'UploadThing is 5x Faster',
|
||||
date: '2024-09-13T12:00:00Z',
|
||||
author: {
|
||||
name: 'Theo Browne',
|
||||
role: 'CEO @ Ping Labs',
|
||||
avatar: 'https://picsum.photos/seed/post1/400/300'
|
||||
},
|
||||
postImage: 'https://picsum.photos/seed/postimage/1200/600', // This can now be undefined or null,
|
||||
tableOfContents: [
|
||||
{ id: 'v7-is-here', title: 'V7 Is Here!' },
|
||||
{ id: 'benchmarks', title: 'Benchmarks' },
|
||||
{ id: 'the-road-to-v7', title: 'The Road To V7' },
|
||||
{ id: 'uploadthing-has-served', title: 'UploadThing Has Served...' },
|
||||
{ id: 'and-were-just-getting-started', title: "...and we're just getting started" }
|
||||
]
|
||||
};
|
||||
let contentLoaded = false;
|
||||
let compiledContent = '';
|
||||
export let data;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const transformed = await import('../test/blog01.md');
|
||||
compiledContent = transformed.default;
|
||||
contentLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('Error compiling markdown:', error);
|
||||
contentLoaded = true;
|
||||
}
|
||||
});
|
||||
$: ({ article, content, loading } = data);
|
||||
$: contentLoaded = !loading && !!content;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.title} - Freedium</title>
|
||||
<title>{article.title} - Freedium</title>
|
||||
<meta name="description" content="Read about the latest updates to UploadThing" />
|
||||
</svelte:head>
|
||||
|
||||
|
|
@ -79,35 +53,31 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if data.postImage}
|
||||
{#if article.postImage}
|
||||
<ImageZoom
|
||||
src={data.postImage}
|
||||
src={article.postImage}
|
||||
alt="Post cover image"
|
||||
class="object-cover w-full h-auto max-h-96"
|
||||
class="object-cover w-full h-auto min-h-96"
|
||||
/>
|
||||
{/if}
|
||||
<header class="p-6 bg-gray-50 dark:bg-zinc-800">
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">{formatDate(data.date)}</p>
|
||||
<h1 class="mb-4 text-4xl font-bold text-gray-900 dark:text-white">{data.title}</h1>
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">{formatDate(article.date)}</p>
|
||||
<h1 class="mb-4 text-4xl font-bold text-gray-900 dark:text-white">{article.title}</h1>
|
||||
<div class="flex items-center">
|
||||
<img src={data.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
|
||||
<img src={article.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{data.author.name}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">{data.author.role}</p>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{article.author.name}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">{article.author.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="p-6 {data.postImage ? '' : 'pt-0'} dark:text-gray-300">
|
||||
<div class="p-6 {article.postImage ? '' : 'pt-0'} dark:text-gray-300">
|
||||
<div class="prose max-w-none">
|
||||
{#if contentLoaded}
|
||||
{#if compiledContent}
|
||||
<svelte:component this={compiledContent} />
|
||||
{:else}
|
||||
<p>Error loading content</p>
|
||||
{/if}
|
||||
{#if content}
|
||||
{@html content}
|
||||
{:else}
|
||||
<p>Loading content...</p>
|
||||
<p>Error loading content</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -132,9 +102,9 @@
|
|||
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Contents
|
||||
</h2>
|
||||
{#if data.tableOfContents && data.tableOfContents.length > 0}
|
||||
{#if article.tableOfContents && article.tableOfContents.length > 0}
|
||||
<ul class="space-y-2">
|
||||
{#each data.tableOfContents as item}
|
||||
{#each article.tableOfContents as item}
|
||||
<li>
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
|
|
@ -155,213 +125,3 @@
|
|||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
<style lang="postcss">
|
||||
/* Headings */
|
||||
:global(.prose h1) {
|
||||
@apply text-4xl font-bold text-gray-900 dark:text-gray-100 mt-12 mb-6;
|
||||
}
|
||||
|
||||
:global(.prose h2) {
|
||||
@apply text-3xl font-bold text-gray-900 dark:text-gray-100 mt-12 mb-4;
|
||||
}
|
||||
|
||||
:global(.prose h3) {
|
||||
@apply text-2xl font-bold text-gray-900 dark:text-gray-100 mt-8 mb-3;
|
||||
}
|
||||
|
||||
:global(.prose h4) {
|
||||
@apply text-xl font-bold text-gray-900 dark:text-gray-100 mt-6 mb-2;
|
||||
}
|
||||
|
||||
:global(.prose h5) {
|
||||
@apply text-lg font-bold text-gray-900 dark:text-gray-100 mt-4 mb-2;
|
||||
}
|
||||
|
||||
:global(.prose h6) {
|
||||
@apply text-base font-bold text-gray-900 dark:text-gray-100 mt-4 mb-2;
|
||||
}
|
||||
|
||||
/* Paragraphs and spacing */
|
||||
:global(.prose p) {
|
||||
@apply text-gray-700 dark:text-gray-300 leading-7 mt-4;
|
||||
}
|
||||
|
||||
:global(.prose > *:first-child) {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
:global(.prose ul) {
|
||||
@apply list-disc list-outside ml-6 mt-4 space-y-2;
|
||||
}
|
||||
|
||||
:global(.prose ol) {
|
||||
@apply list-decimal list-outside ml-6 mt-4 space-y-2;
|
||||
}
|
||||
|
||||
:global(.prose li) {
|
||||
@apply text-gray-700 dark:text-gray-300 leading-7;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
:global(.prose blockquote) {
|
||||
@apply border-l-4 border-gray-300 dark:border-gray-700 pl-4 italic my-6;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
:global(.prose pre) {
|
||||
@apply rounded-lg p-4 my-6 overflow-x-auto;
|
||||
}
|
||||
|
||||
:global(.prose code:not(.shiki code)) {
|
||||
@apply px-1.5 py-0.5 rounded text-sm font-mono bg-gray-200 dark:bg-zinc-700;
|
||||
}
|
||||
|
||||
:global(.prose pre code) {
|
||||
@apply bg-transparent p-0 text-sm leading-relaxed;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
:global(.prose table) {
|
||||
@apply w-full border-collapse my-6;
|
||||
}
|
||||
|
||||
:global(.prose th) {
|
||||
@apply border border-gray-300 dark:border-gray-700 px-4 py-2 bg-gray-100 dark:bg-zinc-800 text-left;
|
||||
}
|
||||
|
||||
:global(.prose td) {
|
||||
@apply border border-gray-300 dark:border-gray-700 px-4 py-2;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
:global(.prose a) {
|
||||
@apply text-primary hover:text-primary/90 underline decoration-primary/30 hover:decoration-primary/50;
|
||||
}
|
||||
|
||||
:global(.prose a::after) {
|
||||
content: '';
|
||||
@apply icon-[heroicons-outline--external-link] inline-block ml-1 w-3.5 h-3.5 align-text-bottom;
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
:global(.prose hr) {
|
||||
@apply border-gray-200 dark:border-gray-700 my-8;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
:global(.prose img) {
|
||||
@apply rounded-lg my-6 mx-auto;
|
||||
}
|
||||
|
||||
/* Inline elements */
|
||||
:global(.prose strong) {
|
||||
@apply font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
:global(.prose em) {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
:global(.prose del) {
|
||||
@apply line-through;
|
||||
}
|
||||
|
||||
/* Definition lists */
|
||||
:global(.prose dl) {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
:global(.prose dt) {
|
||||
@apply font-bold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
:global(.prose dd) {
|
||||
@apply ml-4 mt-2;
|
||||
}
|
||||
|
||||
/* Nested lists spacing */
|
||||
:global(.prose li > ul),
|
||||
:global(.prose li > ol) {
|
||||
@apply mt-2 mb-2;
|
||||
}
|
||||
|
||||
/* Code block filename */
|
||||
:global(.prose .filename) {
|
||||
@apply text-sm text-gray-500 dark:text-gray-400 -mb-2 mt-6;
|
||||
}
|
||||
|
||||
/* Summary blocks */
|
||||
:global(.prose summary) {
|
||||
@apply text-gray-700 dark:text-gray-300 leading-7 cursor-pointer hover:text-gray-900 dark:hover:text-gray-100 select-none;
|
||||
}
|
||||
|
||||
:global(.prose details) {
|
||||
@apply my-4 p-4 rounded-lg bg-gray-50 dark:bg-zinc-800;
|
||||
}
|
||||
|
||||
:global(.prose details[open] summary) {
|
||||
@apply mb-3;
|
||||
}
|
||||
|
||||
/* Aside blocks */
|
||||
:global(.prose aside) {
|
||||
@apply my-6 p-4 rounded-lg border-l-4 border-primary bg-primary/5;
|
||||
}
|
||||
|
||||
:global(.prose aside p) {
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
/* Mark blocks */
|
||||
:global(.prose mark) {
|
||||
@apply bg-primary/20 text-primary px-1.5 py-0.5 rounded;
|
||||
}
|
||||
|
||||
/* Keyboard blocks */
|
||||
:global(.prose kbd) {
|
||||
@apply px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500;
|
||||
}
|
||||
|
||||
/* Mixtape embeds */
|
||||
:global(.prose .mixtape-embed) {
|
||||
@apply items-center p-2 overflow-hidden border border-gray-300 mt-7;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-content) {
|
||||
@apply flex flex-row justify-between p-2 overflow-hidden;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-text) {
|
||||
@apply flex flex-col justify-center p-2;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-text h2) {
|
||||
@apply text-base font-bold text-black dark:text-gray-100;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-description) {
|
||||
@apply block mt-2;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-description h3) {
|
||||
@apply text-sm text-gray-700;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-site) {
|
||||
@apply mt-5;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-site p) {
|
||||
@apply text-xs text-gray-700;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-image) {
|
||||
@apply relative flex h-40 flex-row w-60;
|
||||
}
|
||||
|
||||
:global(.prose .mixtape-image-inner) {
|
||||
@apply absolute inset-0 bg-center bg-cover;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
<DropdownMenu.Label>Browser Extensions</DropdownMenu.Label>
|
||||
<DropdownMenu.Item class="text-xs text-gray-600 cursor-default dark:text-gray-500">
|
||||
<span
|
||||
>Note: These solutions are not developed or controlled by the Freedium developers team. Use
|
||||
at your own risk.</span
|
||||
>Note: These solutions are <span class="border-b border-dashed border-zinc-900">not</span> developed or
|
||||
controlled by the Freedium developers team. Use at your own risk.</span
|
||||
>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item class="text-xs text-gray-600 cursor-default dark:text-gray-500">
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
id="header"
|
||||
class="sticky top-0 z-20 w-full border-b shadow-sm bg-white/95 backdrop-blur-sm dark:bg-zinc-900/95 border-zinc-200 dark:border-zinc-800"
|
||||
>
|
||||
<Advertise />
|
||||
<ProgressLine />
|
||||
|
||||
<div class="container flex items-center justify-between h-16 px-4 mx-auto space-x-2">
|
||||
|
|
@ -54,13 +53,21 @@
|
|||
</Button>
|
||||
<ExtensionsButton />
|
||||
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
|
||||
<PayButtons name="Ko-fi" url="https://ko-fi.com/zhymabekroman" icon="icon-[teenyicons--cup-solid]" />
|
||||
<PayButtons
|
||||
name="Ko-fi"
|
||||
url="https://ko-fi.com/zhymabekroman"
|
||||
icon="icon-[teenyicons--cup-solid]"
|
||||
/>
|
||||
<PayButtons
|
||||
name="Liberapay"
|
||||
url="https://liberapay.com/ZhymabekRoman/"
|
||||
icon="icon-[simple-icons--liberapay]"
|
||||
/>
|
||||
<PayButtons name="Discord" url="https://discord.gg/dAxCuG9nYM" icon="icon-[simple-icons--discord]" />
|
||||
<PayButtons
|
||||
name="Discord"
|
||||
url="https://discord.gg/dAxCuG9nYM"
|
||||
icon="icon-[simple-icons--discord]"
|
||||
/>
|
||||
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
|
||||
<ThemeToggle />
|
||||
<ReportProblem />
|
||||
|
|
@ -101,12 +108,18 @@
|
|||
url="https://liberapay.com/ZhymabekRoman/"
|
||||
icon="icon-[simple-icons--liberapay]"
|
||||
/>
|
||||
<PayButtons name="Discord" url="discord.gg/dAxCuG9nYM" icon="icon-[simple-icons--discord]" />
|
||||
<PayButtons
|
||||
name="Discord"
|
||||
url="discord.gg/dAxCuG9nYM"
|
||||
icon="icon-[simple-icons--discord]"
|
||||
/>
|
||||
<!-- <ExtensionsButton /> -->
|
||||
<ReportProblem />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Advertise />
|
||||
</nav>
|
||||
|
||||
<SearchDialog bind:open={isSearchOpen} />
|
||||
|
|
|
|||
39
new-web/src/routes/+page.server.js
Normal file
39
new-web/src/routes/+page.server.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { render } from "@/services";
|
||||
import { compile } from "mdsvex";
|
||||
|
||||
export async function load({ params }) {
|
||||
try {
|
||||
const transformed = await render("medium");
|
||||
const { code } = await compile(transformed.text);
|
||||
|
||||
return {
|
||||
content: code,
|
||||
article: {
|
||||
title: "UploadThing is 5x Faster",
|
||||
date: "2024-09-13T12:00:00Z",
|
||||
author: {
|
||||
name: "Theo Browne",
|
||||
role: "CEO @ Ping Labs",
|
||||
avatar: "https://picsum.photos/seed/post1/400/300",
|
||||
},
|
||||
postImage: "https://picsum.photos/seed/postimage/1200/600",
|
||||
tableOfContents: [
|
||||
{ id: "v7-is-here", title: "V7 Is Here!" },
|
||||
{ id: "benchmarks", title: "Benchmarks" },
|
||||
{ id: "the-road-to-v7", title: "The Road To V7" },
|
||||
{ id: "uploadthing-has-served", title: "UploadThing Has Served..." },
|
||||
{
|
||||
id: "and-were-just-getting-started",
|
||||
title: "...and we're just getting started",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: null,
|
||||
article: null,
|
||||
error: "Failed to load article",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export const load = ({ params }) => {
|
||||
return {
|
||||
slug: params.slug
|
||||
};
|
||||
};
|
||||
149
new-web/src/routes/[slug]/+page.server.js
Normal file
149
new-web/src/routes/[slug]/+page.server.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { render } from "@/services";
|
||||
import { compile } from "mdsvex";
|
||||
import { createHighlighter } from "shiki";
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
import { h } from "hastscript";
|
||||
|
||||
const HIGHLIGHT_CONFIG = {
|
||||
themes: ["github-light", "github-dark"],
|
||||
langs: ["javascript", "typescript"],
|
||||
};
|
||||
|
||||
const CODE_ATTRIBUTES = {
|
||||
contenteditable: "true",
|
||||
"aria-label": "code",
|
||||
"aria-readonly": "true",
|
||||
inputmode: "none",
|
||||
tabindex: "0",
|
||||
"aria-multiline": "true",
|
||||
"aria-haspopup": "false",
|
||||
"data-gramm": "false",
|
||||
"data-gramm_editor": "false",
|
||||
"data-enable-grammarly": "false",
|
||||
spellcheck: "false",
|
||||
autocorrect: "off",
|
||||
autocapitalize: "none",
|
||||
autocomplete: "off",
|
||||
"data-ms-editor": "false",
|
||||
};
|
||||
|
||||
const MOCK_ARTICLE = {
|
||||
title: "UploadThing is 5x Faster",
|
||||
date: "2024-09-13T12:00:00Z",
|
||||
author: {
|
||||
name: "Theo Browne",
|
||||
role: "CEO @ Ping Labs",
|
||||
avatar: "https://picsum.photos/seed/post1/400/300",
|
||||
},
|
||||
postImage: "https://picsum.photos/seed/postimage/1200/600",
|
||||
tableOfContents: [
|
||||
{ id: "v7-is-here", title: "V7 Is Here!" },
|
||||
{ id: "benchmarks", title: "Benchmarks" },
|
||||
{ id: "the-road-to-v7", title: "The Road To V7" },
|
||||
{ id: "uploadthing-has-served", title: "UploadThing Has Served..." },
|
||||
{
|
||||
id: "and-were-just-getting-started",
|
||||
title: "...and we're just getting started",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function createCodeCopyButton(code, toggleMs = 3000) {
|
||||
const lineCount = code.split("\n").length;
|
||||
const positionClass = lineCount <= 3 ? "top-1/2 -translate-y-1/2" : "top-3";
|
||||
|
||||
const button = h(
|
||||
"button",
|
||||
{
|
||||
"data-code": code,
|
||||
"data-toggle-ms": toggleMs,
|
||||
class: `code-copy-btn absolute right-4 ${positionClass} h-8 w-8 p-1.5 flex items-center justify-center bg-black/50 rounded-md transition-colors duration-200 cursor-pointer hover:bg-black/70`,
|
||||
},
|
||||
[
|
||||
h("span", {
|
||||
class:
|
||||
"ready text-white w-full aspect-square bg-no-repeat bg-center bg-cover z-50 block icon-[heroicons--clipboard-document]",
|
||||
}),
|
||||
h("span", {
|
||||
class:
|
||||
"success w-full text-white aspect-square bg-no-repeat bg-center bg-cover z-50 hidden icon-[heroicons--clipboard-document-check-16-solid]",
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
return toHtml(button);
|
||||
}
|
||||
|
||||
async function createHighlightedCode(code, lang = "text") {
|
||||
const highlighter = await createHighlighter(HIGHLIGHT_CONFIG);
|
||||
await highlighter.loadLanguage("javascript", "typescript");
|
||||
|
||||
const lightHtml = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: "github-light",
|
||||
transformers: [
|
||||
{
|
||||
code(node) {
|
||||
node.properties = { ...node.properties, ...CODE_ATTRIBUTES };
|
||||
return node;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const darkHtml = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: "github-dark",
|
||||
transformers: [
|
||||
{
|
||||
code(node) {
|
||||
node.properties = { ...node.properties, ...CODE_ATTRIBUTES };
|
||||
return node;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const buttonHtml = createCodeCopyButton(code, 1200);
|
||||
|
||||
return `
|
||||
<div class="relative">
|
||||
${buttonHtml}
|
||||
<div class="dark:hidden">${lightHtml}</div>
|
||||
<div class="hidden dark:block">${darkHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export async function load({ params }) {
|
||||
try {
|
||||
const transformed = await render("medium");
|
||||
const { code } = await compile(transformed.text, {
|
||||
highlight: {
|
||||
highlighter: createHighlightedCode,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
slug: params.slug,
|
||||
loading: false,
|
||||
content: code,
|
||||
article: MOCK_ARTICLE,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load article:", error);
|
||||
return {
|
||||
slug: params.slug,
|
||||
loading: false,
|
||||
content: null,
|
||||
article: {
|
||||
title: "Article Not Found",
|
||||
date: new Date().toISOString(),
|
||||
author: { name: "Unknown", role: "", avatar: "" },
|
||||
postImage: null,
|
||||
tableOfContents: [],
|
||||
},
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initializeCodeCopyButtons } from '$lib/codeCopy';
|
||||
|
||||
onMount(() => {
|
||||
initializeCodeCopyButtons();
|
||||
});
|
||||
import ArticlePage from '$lib/elements/ArticlePage.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<ArticlePage />
|
||||
<ArticlePage {data} />
|
||||
|
|
|
|||
35
new-web/src/routes/api/services/[service]/render/+server.js
Normal file
35
new-web/src/routes/api/services/[service]/render/+server.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { json } from "@sveltejs/kit";
|
||||
import { compile } from "mdsvex";
|
||||
|
||||
const mdsvexConfig = {
|
||||
extensions: [".svx", ".md"],
|
||||
smartypants: {
|
||||
dashes: "oldschool",
|
||||
},
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
};
|
||||
|
||||
export async function GET({ params }) {
|
||||
try {
|
||||
const { service } = params;
|
||||
|
||||
// Example content - replace with your actual content fetching
|
||||
const markdown = `
|
||||
# Test Content ${service}
|
||||
This is a test article.
|
||||
`;
|
||||
|
||||
const compiled = await compile(markdown, mdsvexConfig);
|
||||
|
||||
if (!compiled) {
|
||||
throw new Error("Failed to compile markdown");
|
||||
}
|
||||
|
||||
return json({
|
||||
text: compiled.code,
|
||||
});
|
||||
} catch (error) {
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
1
new-web/src/services/index.js
Normal file
1
new-web/src/services/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { render } from "./render";
|
||||
8
new-web/src/services/render.js
Normal file
8
new-web/src/services/render.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import apiFetch from "@/api";
|
||||
|
||||
async function render(serviceName) {
|
||||
const response = await apiFetch(`/services/${serviceName}/render`);
|
||||
return response;
|
||||
}
|
||||
|
||||
export { render };
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { mdsvex, escapeSvelte } from "mdsvex";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
import { createHighlighter } from "shiki";
|
||||
import adapter from "@sveltejs/adapter-auto";
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import { h } from "hastscript";
|
||||
import remarkMixtapeEmbed from "./src/lib/utils/remark/mixtape-embed.js";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
import rehypeShiki from "@shikijs/rehype";
|
||||
|
||||
const readySvg =
|
||||
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'><path fill='none' stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192q.56-.045 1.124-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48 48 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.25 2.25 0 0 0 15 2.25h-1.5a2.25 2.25 0 0 0-2.15 1.586m5.8 0q.099.316.1.664v.75h-6V4.5q.001-.348.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9'/></svg>";
|
||||
|
|
@ -95,6 +96,7 @@ const mdsvexOptions = {
|
|||
remarkPlugins: [remarkMixtapeEmbed],
|
||||
rehypePlugins: [
|
||||
[rehypeExternalLinks, { target: "_blank", rel: ["nofollow"] }],
|
||||
[rehypeShiki, { theme: "poimandres" }],
|
||||
],
|
||||
highlight: {
|
||||
highlighter: async (code, lang = "text") => {
|
||||
|
|
@ -134,10 +136,8 @@ const mdsvexOptions = {
|
|||
});
|
||||
const resultHtml = toHtml(result);
|
||||
const buttonHtml = renderCodeCopyButton(code, { toggle: 1200 });
|
||||
const html = escapeSvelte(
|
||||
`<div class="relative">${buttonHtml}${resultHtml}</div>`,
|
||||
);
|
||||
return `{@html \`${html}\` }`;
|
||||
const html = `<div class="relative">${buttonHtml}${resultHtml}</div>`;
|
||||
return html;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import path from 'path';
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib: path.resolve('./src/lib')
|
||||
}
|
||||
}
|
||||
$lib: path.resolve("./src/lib"),
|
||||
"@": path.resolve("./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue