Compare commits
27 Commits
active-fol
...
org-roam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86eaaae945 | ||
|
|
ec00a40aef | ||
|
|
25a6747d7d | ||
|
|
b4fb0e6682 | ||
|
|
f346a01296 | ||
|
|
c2dcc63b5f | ||
|
|
c2bea8a4c4 | ||
|
|
fa8d87a23a | ||
|
|
65c5b27041 | ||
|
|
5208a96a37 | ||
|
|
31ea7852fd | ||
|
|
7dc826be0a | ||
|
|
11ab6da80c | ||
|
|
de1e7505ba | ||
|
|
9c042dd717 | ||
|
|
bacd19c4ea | ||
|
|
722277b202 | ||
|
|
e6cc9ba368 | ||
|
|
643aca5ffa | ||
|
|
ec26ebcc9e | ||
|
|
19e324d914 | ||
|
|
368203cf85 | ||
|
|
13ff64db97 | ||
|
|
87f7f4804e | ||
|
|
c99c8070f2 | ||
|
|
e7d2a57aad | ||
|
|
ef29c69828 |
6
.github/workflows/build-preview.yaml
vendored
6
.github/workflows/build-preview.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build Preview
|
name: Build Preview
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: npx quartz build -d docs -v
|
run: npx quartz build -d docs -v
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: preview-build
|
name: preview-build
|
||||||
path: public
|
path: public
|
||||||
|
|||||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
|
|||||||
2
.github/workflows/deploy-preview.yaml
vendored
2
.github/workflows/deploy-preview.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
name: Deploy Preview to Cloudflare Pages
|
name: Deploy Preview to Cloudflare Pages
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Download build artifact
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v7
|
||||||
id: preview-build-artifact
|
id: preview-build-artifact
|
||||||
with:
|
with:
|
||||||
name: preview-build
|
name: preview-build
|
||||||
|
|||||||
4
.github/workflows/docker-build-push.yaml
vendored
4
.github/workflows/docker-build-push.yaml
vendored
@@ -21,11 +21,11 @@ jobs:
|
|||||||
echo "OWNER_LOWERCASE=${OWNER,,}" >> ${GITHUB_ENV}
|
echo "OWNER_LOWERCASE=${OWNER,,}" >> ${GITHUB_ENV}
|
||||||
env:
|
env:
|
||||||
OWNER: "${{ github.repository_owner }}"
|
OWNER: "${{ github.repository_owner }}"
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- name: Inject slug/short variables
|
- name: Inject slug/short variables
|
||||||
uses: rlespinasse/github-slug-action@v5.3.0
|
uses: rlespinasse/github-slug-action@v5.4.0
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
242
AGENTS.md
Normal file
242
AGENTS.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# AGENTS.md - Coding Agent Instructions
|
||||||
|
|
||||||
|
This document provides essential information for AI coding agents working in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Quartz** is a static site generator for publishing digital gardens and notes as websites.
|
||||||
|
Built with TypeScript, Preact, and unified/remark/rehype for markdown processing.
|
||||||
|
|
||||||
|
| Stack | Technology |
|
||||||
|
| ------------- | ----------------------------------------- |
|
||||||
|
| Language | TypeScript 5.x (strict mode) |
|
||||||
|
| Runtime | Node.js >=22 (v22.16.0 pinned) |
|
||||||
|
| Package Mgr | npm >=10.9.2 |
|
||||||
|
| Module System | ES Modules (`"type": "module"`) |
|
||||||
|
| UI Framework | Preact 10.x (JSX with `react-jsx` pragma) |
|
||||||
|
| Build Tool | esbuild |
|
||||||
|
| Styling | SCSS via esbuild-sass-plugin |
|
||||||
|
|
||||||
|
## Build, Lint, and Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Type check and format check (CI validation)
|
||||||
|
npm run check
|
||||||
|
|
||||||
|
# Auto-format code with Prettier
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
npx tsx --test quartz/util/path.test.ts
|
||||||
|
|
||||||
|
# Run tests matching a pattern (use --test-name-pattern)
|
||||||
|
npx tsx --test --test-name-pattern="typeguards" quartz/util/path.test.ts
|
||||||
|
|
||||||
|
# Build the static site
|
||||||
|
npx quartz build
|
||||||
|
|
||||||
|
# Build and serve with hot reload
|
||||||
|
npx quartz build --serve
|
||||||
|
|
||||||
|
# Profile build performance
|
||||||
|
npm run profile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Files Location
|
||||||
|
|
||||||
|
Tests use Node.js native test runner via `tsx`. Test files follow the `*.test.ts` pattern:
|
||||||
|
|
||||||
|
- `quartz/util/path.test.ts`
|
||||||
|
- `quartz/util/fileTrie.test.ts`
|
||||||
|
- `quartz/components/scripts/search.test.ts`
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Prettier Configuration (`.prettierrc`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"quoteProps": "as-needed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**No ESLint** - only Prettier for formatting. Run `npm run format` before committing.
|
||||||
|
|
||||||
|
### TypeScript Configuration
|
||||||
|
|
||||||
|
- **Strict mode enabled** (`strict: true`)
|
||||||
|
- `noUnusedLocals: true` - no unused variables
|
||||||
|
- `noUnusedParameters: true` - no unused function parameters
|
||||||
|
- JSX configured for Preact (`jsxImportSource: "preact"`)
|
||||||
|
|
||||||
|
### Import Conventions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. External packages first
|
||||||
|
import { PluggableList } from "unified"
|
||||||
|
import { visit } from "unist-util-visit"
|
||||||
|
|
||||||
|
// 2. Internal utilities/types (relative paths)
|
||||||
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
import { FilePath, slugifyFilePath } from "../../util/path"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
| Element | Convention | Example |
|
||||||
|
| ---------------- | ------------ | ----------------------------------- |
|
||||||
|
| Files (utils) | camelCase | `path.ts`, `fileTrie.ts` |
|
||||||
|
| Files (comps) | PascalCase | `TableOfContents.tsx`, `Search.tsx` |
|
||||||
|
| Types/Interfaces | PascalCase | `QuartzComponent`, `FullSlug` |
|
||||||
|
| Type Guards | `is*` prefix | `isFilePath()`, `isFullSlug()` |
|
||||||
|
| Constants | UPPER_CASE | `QUARTZ`, `UPSTREAM_NAME` |
|
||||||
|
| Options types | `Options` | `interface Options { ... }` |
|
||||||
|
|
||||||
|
### Branded Types Pattern
|
||||||
|
|
||||||
|
This codebase uses branded types for type-safe path handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type SlugLike<T> = string & { __brand: T }
|
||||||
|
export type FilePath = SlugLike<"filepath">
|
||||||
|
export type FullSlug = SlugLike<"full">
|
||||||
|
export type SimpleSlug = SlugLike<"simple">
|
||||||
|
|
||||||
|
// Always validate with type guards before using
|
||||||
|
export function isFilePath(s: string): s is FilePath { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Pattern (Preact)
|
||||||
|
|
||||||
|
Components use a factory function pattern with attached static properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
|
const opts: Options = { ...defaultOptions, ...userOpts }
|
||||||
|
|
||||||
|
const ComponentName: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
|
||||||
|
return <div class={classNames(displayClass, "component-name")}>...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentName.css = style // SCSS styles
|
||||||
|
ComponentName.afterDOMLoaded = script // Client-side JS
|
||||||
|
return ComponentName
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Pattern
|
||||||
|
|
||||||
|
Three plugin types: transformers, filters, and emitters.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const PluginName: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
|
return {
|
||||||
|
name: "PluginName",
|
||||||
|
markdownPlugins(ctx) { return [...] },
|
||||||
|
htmlPlugins(ctx) { return [...] },
|
||||||
|
externalResources(ctx) { return { js: [], css: [] } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Pattern
|
||||||
|
|
||||||
|
Use Node.js native test runner with `assert`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import test, { describe, beforeEach } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
|
||||||
|
describe("FeatureName", () => {
|
||||||
|
test("should do something", () => {
|
||||||
|
assert.strictEqual(actual, expected)
|
||||||
|
assert.deepStrictEqual(actualObj, expectedObj)
|
||||||
|
assert(condition) // truthy assertion
|
||||||
|
assert(!condition) // falsy assertion
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Use `try/catch` for critical operations (file I/O, parsing)
|
||||||
|
- Custom `trace` utility for error reporting with stack traces
|
||||||
|
- `process.exit(1)` for fatal errors
|
||||||
|
- `console.warn()` for non-fatal issues
|
||||||
|
|
||||||
|
### Async Patterns
|
||||||
|
|
||||||
|
- Prefer `async/await` over raw promises
|
||||||
|
- Use async generators (`async *emit()`) for streaming file output
|
||||||
|
- Use `async-mutex` for concurrent build protection
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
quartz/
|
||||||
|
├── bootstrap-cli.mjs # CLI entry point
|
||||||
|
├── build.ts # Build orchestration
|
||||||
|
├── cfg.ts # Configuration types
|
||||||
|
├── components/ # Preact UI components
|
||||||
|
│ ├── *.tsx # Components
|
||||||
|
│ ├── scripts/ # Client-side scripts (*.inline.ts)
|
||||||
|
│ └── styles/ # Component SCSS
|
||||||
|
├── plugins/
|
||||||
|
│ ├── transformers/ # Markdown AST transformers
|
||||||
|
│ ├── filters/ # Content filters
|
||||||
|
│ ├── emitters/ # Output generators
|
||||||
|
│ └── types.ts # Plugin type definitions
|
||||||
|
├── processors/ # Build pipeline (parse/filter/emit)
|
||||||
|
├── util/ # Utility functions
|
||||||
|
└── i18n/ # Internationalization (30+ locales)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branch Workflow
|
||||||
|
|
||||||
|
This is a fork of [jackyzha0/quartz](https://github.com/jackyzha0/quartz) with org-roam customizations.
|
||||||
|
|
||||||
|
| Branch | Purpose |
|
||||||
|
| ----------- | ------------------------------------------------ |
|
||||||
|
| `main` | Clean mirror of upstream quartz — no custom code |
|
||||||
|
| `org-roam` | Default branch — all customizations live here |
|
||||||
|
| `feature/*` | Short-lived branches off `org-roam` |
|
||||||
|
|
||||||
|
### Pulling Upstream Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git fetch upstream
|
||||||
|
git merge upstream/main
|
||||||
|
git checkout org-roam
|
||||||
|
git merge main
|
||||||
|
# Resolve conflicts if any, then commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working on Features
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout org-roam
|
||||||
|
git checkout -b feature/my-feature
|
||||||
|
# ... work ...
|
||||||
|
git checkout org-roam
|
||||||
|
git merge feature/my-feature
|
||||||
|
git branch -d feature/my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
**Merge direction:** `upstream → main → org-roam → feature/*`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Client-side scripts**: Use `.inline.ts` suffix, bundled via esbuild
|
||||||
|
- **Isomorphic code**: `quartz/util/path.ts` must not use Node.js APIs
|
||||||
|
- **Incremental builds**: Plugins can implement `partialEmit` for efficiency
|
||||||
|
- **Markdown flavors**: Supports Obsidian (`ofm.ts`) and Roam (`roam.ts`) syntax
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
|
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
|
||||||
|
|
||||||
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
||||||
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
|
||||||
|
|
||||||
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
|||||||
- `{provider: 'clarity', projectId: '<your-clarity-id-code' }`: use [Microsoft clarity](https://clarity.microsoft.com/). The project id can be found on top of the overview page.
|
- `{provider: 'clarity', projectId: '<your-clarity-id-code' }`: use [Microsoft clarity](https://clarity.microsoft.com/). The project id can be found on top of the overview page.
|
||||||
- `{ provider: 'matomo', siteId: '<your-matomo-id-code', host: 'matomo.example.com' }`: use [Matomo](https://matomo.org/), without protocol.
|
- `{ provider: 'matomo', siteId: '<your-matomo-id-code', host: 'matomo.example.com' }`: use [Matomo](https://matomo.org/), without protocol.
|
||||||
- `{ provider: 'vercel' }`: use [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics).
|
- `{ provider: 'vercel' }`: use [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics).
|
||||||
|
- `{ provider: 'rybbit', siteId: 'my-rybbit-id' }` (managed) or `{ provider: 'rybbit', siteId: 'my-rybbit-id', host: 'my-rybbit-domain.com' }` (self-hosted) use [Rybbit](https://rybbit.com);
|
||||||
- `locale`: used for [[i18n]] and date formatting
|
- `locale`: used for [[i18n]] and date formatting
|
||||||
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
||||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`.
|
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`.
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ You can run the below one-liner to run Quartz in Docker.
|
|||||||
```sh
|
```sh
|
||||||
docker run --rm -itp 8080:8080 -p 3001:3001 -v ./content:/usr/src/app/content $(docker build -q .)
|
docker run --rm -itp 8080:8080 -p 3001:3001 -v ./content:/usr/src/app/content $(docker build -q .)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!warning] Not to be used for production
|
||||||
|
> Serve mode is intended for local previews only.
|
||||||
|
> For production workloads, see the page on [[hosting]].
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ You can access the tags of a file by `node.data.tags`.
|
|||||||
Component.Explorer({
|
Component.Explorer({
|
||||||
filterFn: (node) => {
|
filterFn: (node) => {
|
||||||
// exclude files with the tag "explorerexclude"
|
// exclude files with the tag "explorerexclude"
|
||||||
return node.data.tags?.includes("explorerexclude") !== true
|
return node.data?.tags?.includes("explorerexclude") !== true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ By default, Quartz only fetches previews for pages inside your vault due to [COR
|
|||||||
|
|
||||||
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
|
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
|
||||||
|
|
||||||
Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups.
|
Similar to Obsidian, [[quartz-layout-desktop.png|images referenced using wikilinks]] can also be viewed as popups.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
1484
package-lock.json
generated
1484
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -42,28 +42,28 @@
|
|||||||
"@tweenjs/tween.js": "^25.0.0",
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"ansi-truncate": "^1.4.0",
|
"ansi-truncate": "^1.4.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^5.0.0",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"esbuild-sass-plugin": "^3.3.1",
|
"esbuild-sass-plugin": "^3.6.0",
|
||||||
"flexsearch": "^0.8.205",
|
"flexsearch": "^0.8.205",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^15.0.0",
|
"globby": "^16.1.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.5",
|
"hast-util-to-html": "^9.0.5",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||||
"hast-util-to-string": "^3.0.1",
|
"hast-util-to-string": "^3.0.1",
|
||||||
"is-absolute-url": "^5.0.0",
|
"is-absolute-url": "^5.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.1",
|
||||||
"lightningcss": "^1.30.2",
|
"lightningcss": "^1.31.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.2",
|
"mdast-util-find-and-replace": "^3.0.2",
|
||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.1",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"minimatch": "^10.1.1",
|
"minimatch": "^10.1.1",
|
||||||
"pixi.js": "^8.14.0",
|
"pixi.js": "^8.15.0",
|
||||||
"preact": "^10.27.2",
|
"preact": "^10.28.2",
|
||||||
"preact-render-to-string": "^6.6.3",
|
"preact-render-to-string": "^6.6.5",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
@@ -83,32 +83,32 @@
|
|||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
"remark-smartypants": "^3.0.2",
|
"remark-smartypants": "^3.0.2",
|
||||||
"rfdc": "^1.4.1",
|
"rfdc": "^1.4.1",
|
||||||
"satori": "^0.18.3",
|
"satori": "^0.19.1",
|
||||||
"serve-handler": "^6.1.6",
|
"serve-handler": "^6.1.6",
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.5",
|
||||||
"shiki": "^1.26.2",
|
"shiki": "^1.26.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^8.0.0",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.1.0",
|
||||||
"vfile": "^6.0.3",
|
"vfile": "^6.0.3",
|
||||||
"workerpool": "^10.0.0",
|
"workerpool": "^10.0.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.19.0",
|
||||||
"yargs": "^18.0.0"
|
"yargs": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^25.0.10",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/yargs": "^17.0.34",
|
"@types/yargs": "^17.0.35",
|
||||||
"esbuild": "^0.25.12",
|
"esbuild": "^0.27.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.8.1",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ async function startWatching(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
const watcher = chokidar.watch(".", {
|
||||||
|
awaitWriteFinish: { stabilityThreshold: 250 },
|
||||||
persistent: true,
|
persistent: true,
|
||||||
cwd: argv.directory,
|
cwd: argv.directory,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ export type Analytics =
|
|||||||
| {
|
| {
|
||||||
provider: "vercel"
|
provider: "vercel"
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
provider: "rybbit"
|
||||||
|
siteId: string
|
||||||
|
host?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
pageTitle: string
|
pageTitle: string
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import fs from "fs"
|
|||||||
export function escapePath(fp) {
|
export function escapePath(fp) {
|
||||||
return fp
|
return fp
|
||||||
.replace(/\\ /g, " ") // unescape spaces
|
.replace(/\\ /g, " ") // unescape spaces
|
||||||
.replace(/^".*"$/, "$1")
|
.replace(/^"(.*)"$/, "$1")
|
||||||
.replace(/^'.*"$/, "$1")
|
.replace(/^'(.*)'$/, "$1")
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export function renderPage(
|
|||||||
</body>
|
</body>
|
||||||
{pageResources.js
|
{pageResources.js
|
||||||
.filter((resource) => resource.loadTime === "afterDOMReady")
|
.filter((resource) => resource.loadTime === "afterDOMReady")
|
||||||
.map((res) => JSResourceToScriptElement(res))}
|
.map((res) => JSResourceToScriptElement(res, true))}
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ function createFolderNode(
|
|||||||
const folderPath = node.slug
|
const folderPath = node.slug
|
||||||
folderContainer.dataset.folderpath = folderPath
|
folderContainer.dataset.folderpath = folderPath
|
||||||
|
|
||||||
|
if (currentSlug === folderPath) {
|
||||||
|
folderContainer.classList.add("active")
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.folderClickBehavior === "link") {
|
if (opts.folderClickBehavior === "link") {
|
||||||
// Replace button with link for link behavior
|
// Replace button with link for link behavior
|
||||||
const button = titleContainer.querySelector(".folder-button") as HTMLElement
|
const button = titleContainer.querySelector(".folder-button") as HTMLElement
|
||||||
|
|||||||
@@ -29,17 +29,31 @@ class DiagramPanZoom {
|
|||||||
const mouseDownHandler = this.onMouseDown.bind(this)
|
const mouseDownHandler = this.onMouseDown.bind(this)
|
||||||
const mouseMoveHandler = this.onMouseMove.bind(this)
|
const mouseMoveHandler = this.onMouseMove.bind(this)
|
||||||
const mouseUpHandler = this.onMouseUp.bind(this)
|
const mouseUpHandler = this.onMouseUp.bind(this)
|
||||||
|
|
||||||
|
// Touch drag events
|
||||||
|
const touchStartHandler = this.onTouchStart.bind(this)
|
||||||
|
const touchMoveHandler = this.onTouchMove.bind(this)
|
||||||
|
const touchEndHandler = this.onTouchEnd.bind(this)
|
||||||
|
|
||||||
const resizeHandler = this.resetTransform.bind(this)
|
const resizeHandler = this.resetTransform.bind(this)
|
||||||
|
|
||||||
this.container.addEventListener("mousedown", mouseDownHandler)
|
this.container.addEventListener("mousedown", mouseDownHandler)
|
||||||
document.addEventListener("mousemove", mouseMoveHandler)
|
document.addEventListener("mousemove", mouseMoveHandler)
|
||||||
document.addEventListener("mouseup", mouseUpHandler)
|
document.addEventListener("mouseup", mouseUpHandler)
|
||||||
|
|
||||||
|
this.container.addEventListener("touchstart", touchStartHandler, { passive: false })
|
||||||
|
document.addEventListener("touchmove", touchMoveHandler, { passive: false })
|
||||||
|
document.addEventListener("touchend", touchEndHandler)
|
||||||
|
|
||||||
window.addEventListener("resize", resizeHandler)
|
window.addEventListener("resize", resizeHandler)
|
||||||
|
|
||||||
this.cleanups.push(
|
this.cleanups.push(
|
||||||
() => this.container.removeEventListener("mousedown", mouseDownHandler),
|
() => this.container.removeEventListener("mousedown", mouseDownHandler),
|
||||||
() => document.removeEventListener("mousemove", mouseMoveHandler),
|
() => document.removeEventListener("mousemove", mouseMoveHandler),
|
||||||
() => document.removeEventListener("mouseup", mouseUpHandler),
|
() => document.removeEventListener("mouseup", mouseUpHandler),
|
||||||
|
() => this.container.removeEventListener("touchstart", touchStartHandler),
|
||||||
|
() => document.removeEventListener("touchmove", touchMoveHandler),
|
||||||
|
() => document.removeEventListener("touchend", touchEndHandler),
|
||||||
() => window.removeEventListener("resize", resizeHandler),
|
() => window.removeEventListener("resize", resizeHandler),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,6 +113,30 @@ class DiagramPanZoom {
|
|||||||
this.container.style.cursor = "grab"
|
this.container.style.cursor = "grab"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onTouchStart(e: TouchEvent) {
|
||||||
|
if (e.touches.length !== 1) return
|
||||||
|
this.isDragging = true
|
||||||
|
const touch = e.touches[0]
|
||||||
|
this.startPan = { x: touch.clientX - this.currentPan.x, y: touch.clientY - this.currentPan.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTouchMove(e: TouchEvent) {
|
||||||
|
if (!this.isDragging || e.touches.length !== 1) return
|
||||||
|
e.preventDefault() // Prevent scrolling
|
||||||
|
|
||||||
|
const touch = e.touches[0]
|
||||||
|
this.currentPan = {
|
||||||
|
x: touch.clientX - this.startPan.x,
|
||||||
|
y: touch.clientY - this.startPan.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTransform()
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTouchEnd() {
|
||||||
|
this.isDragging = false
|
||||||
|
}
|
||||||
|
|
||||||
private zoom(delta: number) {
|
private zoom(delta: number) {
|
||||||
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
|
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
|
||||||
|
|
||||||
@@ -120,11 +158,15 @@ class DiagramPanZoom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resetTransform() {
|
private resetTransform() {
|
||||||
this.scale = 1
|
|
||||||
const svg = this.content.querySelector("svg")!
|
const svg = this.content.querySelector("svg")!
|
||||||
|
const rect = svg.getBoundingClientRect()
|
||||||
|
const width = rect.width / this.scale
|
||||||
|
const height = rect.height / this.scale
|
||||||
|
|
||||||
|
this.scale = 1
|
||||||
this.currentPan = {
|
this.currentPan = {
|
||||||
x: svg.getBoundingClientRect().width / 2,
|
x: (this.container.clientWidth - width) / 2,
|
||||||
y: svg.getBoundingClientRect().height / 2,
|
y: (this.container.clientHeight - height) / 2,
|
||||||
}
|
}
|
||||||
this.updateTransform()
|
this.updateTransform()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,49 @@ interface Item {
|
|||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
let currentSearchTerm: string = ""
|
let currentSearchTerm: string = ""
|
||||||
const encoder = (str: string) => {
|
const encoder = (str: string): string[] => {
|
||||||
return str
|
const tokens: string[] = []
|
||||||
.toLowerCase()
|
let bufferStart = -1
|
||||||
.split(/\s+/)
|
let bufferEnd = -1
|
||||||
.filter((token) => token.length > 0)
|
const lower = str.toLowerCase()
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (const char of lower) {
|
||||||
|
const code = char.codePointAt(0)!
|
||||||
|
|
||||||
|
const isCJK =
|
||||||
|
(code >= 0x3040 && code <= 0x309f) ||
|
||||||
|
(code >= 0x30a0 && code <= 0x30ff) ||
|
||||||
|
(code >= 0x4e00 && code <= 0x9fff) ||
|
||||||
|
(code >= 0xac00 && code <= 0xd7af) ||
|
||||||
|
(code >= 0x20000 && code <= 0x2a6df)
|
||||||
|
|
||||||
|
const isWhitespace = code === 32 || code === 9 || code === 10 || code === 13
|
||||||
|
|
||||||
|
if (isCJK) {
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart, bufferEnd))
|
||||||
|
bufferStart = -1
|
||||||
|
}
|
||||||
|
tokens.push(char)
|
||||||
|
} else if (isWhitespace) {
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart, bufferEnd))
|
||||||
|
bufferStart = -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (bufferStart === -1) bufferStart = i
|
||||||
|
bufferEnd = i + char.length
|
||||||
|
}
|
||||||
|
|
||||||
|
i += char.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = new FlexSearch.Document<Item>({
|
let index = new FlexSearch.Document<Item>({
|
||||||
|
|||||||
163
quartz/components/scripts/search.test.ts
Normal file
163
quartz/components/scripts/search.test.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import test, { describe } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
|
||||||
|
// Inline the encoder function from search.inline.ts for testing
|
||||||
|
const encoder = (str: string): string[] => {
|
||||||
|
const tokens: string[] = []
|
||||||
|
let bufferStart = -1
|
||||||
|
let bufferEnd = -1
|
||||||
|
const lower = str.toLowerCase()
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (const char of lower) {
|
||||||
|
const code = char.codePointAt(0)!
|
||||||
|
|
||||||
|
const isCJK =
|
||||||
|
(code >= 0x3040 && code <= 0x309f) ||
|
||||||
|
(code >= 0x30a0 && code <= 0x30ff) ||
|
||||||
|
(code >= 0x4e00 && code <= 0x9fff) ||
|
||||||
|
(code >= 0xac00 && code <= 0xd7af) ||
|
||||||
|
(code >= 0x20000 && code <= 0x2a6df)
|
||||||
|
|
||||||
|
const isWhitespace = code === 32 || code === 9 || code === 10 || code === 13
|
||||||
|
|
||||||
|
if (isCJK) {
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart, bufferEnd))
|
||||||
|
bufferStart = -1
|
||||||
|
}
|
||||||
|
tokens.push(char)
|
||||||
|
} else if (isWhitespace) {
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart, bufferEnd))
|
||||||
|
bufferStart = -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (bufferStart === -1) bufferStart = i
|
||||||
|
bufferEnd = i + char.length
|
||||||
|
}
|
||||||
|
|
||||||
|
i += char.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferStart !== -1) {
|
||||||
|
tokens.push(lower.slice(bufferStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("search encoder", () => {
|
||||||
|
describe("English text", () => {
|
||||||
|
test("should tokenize simple English words", () => {
|
||||||
|
const result = encoder("hello world")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "world"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle multiple spaces", () => {
|
||||||
|
const result = encoder("hello world")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "world"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle tabs and newlines", () => {
|
||||||
|
const result = encoder("hello\tworld\ntest")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "world", "test"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should lowercase all text", () => {
|
||||||
|
const result = encoder("Hello WORLD Test")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "world", "test"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("CJK text", () => {
|
||||||
|
test("should tokenize Japanese Hiragana character by character", () => {
|
||||||
|
const result = encoder("こんにちは")
|
||||||
|
assert.deepStrictEqual(result, ["こ", "ん", "に", "ち", "は"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should tokenize Japanese Katakana character by character", () => {
|
||||||
|
const result = encoder("コントロール")
|
||||||
|
assert.deepStrictEqual(result, ["コ", "ン", "ト", "ロ", "ー", "ル"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should tokenize Japanese Kanji character by character", () => {
|
||||||
|
const result = encoder("日本語")
|
||||||
|
assert.deepStrictEqual(result, ["日", "本", "語"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should tokenize Korean Hangul character by character", () => {
|
||||||
|
const result = encoder("안녕하세요")
|
||||||
|
assert.deepStrictEqual(result, ["안", "녕", "하", "세", "요"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should tokenize Chinese characters character by character", () => {
|
||||||
|
const result = encoder("你好世界")
|
||||||
|
assert.deepStrictEqual(result, ["你", "好", "世", "界"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle mixed Hiragana/Katakana/Kanji", () => {
|
||||||
|
const result = encoder("て以来")
|
||||||
|
assert.deepStrictEqual(result, ["て", "以", "来"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Mixed CJK and English", () => {
|
||||||
|
test("should handle Japanese with English words", () => {
|
||||||
|
const result = encoder("hello 世界")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "世", "界"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle English with Japanese words", () => {
|
||||||
|
const result = encoder("世界 hello world")
|
||||||
|
assert.deepStrictEqual(result, ["世", "界", "hello", "world"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle complex mixed content", () => {
|
||||||
|
const result = encoder("これはtest文章です")
|
||||||
|
assert.deepStrictEqual(result, ["こ", "れ", "は", "test", "文", "章", "で", "す"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle mixed Korean and English", () => {
|
||||||
|
const result = encoder("hello 안녕 world")
|
||||||
|
assert.deepStrictEqual(result, ["hello", "안", "녕", "world"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle mixed Chinese and English", () => {
|
||||||
|
const result = encoder("你好 world")
|
||||||
|
assert.deepStrictEqual(result, ["你", "好", "world"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Edge cases", () => {
|
||||||
|
test("should handle empty string", () => {
|
||||||
|
const result = encoder("")
|
||||||
|
assert.deepStrictEqual(result, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle only whitespace", () => {
|
||||||
|
const result = encoder(" \t\n ")
|
||||||
|
assert.deepStrictEqual(result, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle single character", () => {
|
||||||
|
const result = encoder("a")
|
||||||
|
assert.deepStrictEqual(result, ["a"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle single CJK character", () => {
|
||||||
|
const result = encoder("あ")
|
||||||
|
assert.deepStrictEqual(result, ["あ"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle CJK with trailing whitespace", () => {
|
||||||
|
const result = encoder("日本語 ")
|
||||||
|
assert.deepStrictEqual(result, ["日", "本", "語"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle English with trailing whitespace", () => {
|
||||||
|
const result = encoder("hello ")
|
||||||
|
assert.deepStrictEqual(result, ["hello"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -115,9 +115,9 @@ async function _navigate(url: URL, isBack: boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// now, patch head, re-executing scripts
|
// now, patch head, re-executing scripts
|
||||||
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
|
const elementsToRemove = document.head.querySelectorAll(":not([data-persist])")
|
||||||
elementsToRemove.forEach((el) => el.remove())
|
elementsToRemove.forEach((el) => el.remove())
|
||||||
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
|
const elementsToAdd = html.head.querySelectorAll(":not([data-persist])")
|
||||||
elementsToAdd.forEach((el) => document.head.appendChild(el))
|
elementsToAdd.forEach((el) => document.head.appendChild(el))
|
||||||
|
|
||||||
// delay setting the url until now
|
// delay setting the url until now
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 32px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
& > :not(.sidebar.left:has(.explorer)) {
|
& > :not(.sidebar.left:has(.explorer)) {
|
||||||
transition: transform 300ms ease-in-out;
|
transition: transform 300ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
|
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
|
||||||
transform: translateX(100dvw);
|
transform: translateX(100dvw);
|
||||||
transition: transform 300ms ease-in-out;
|
transition: transform 300ms ease-in-out;
|
||||||
@@ -33,8 +34,10 @@
|
|||||||
|
|
||||||
min-height: 1.2rem;
|
min-height: 1.2rem;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
flex: 0 1 1.2rem;
|
flex: 0 1 1.2rem;
|
||||||
|
|
||||||
& .fold {
|
& .fold {
|
||||||
transform: rotateZ(-90deg);
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
@@ -118,7 +121,10 @@ button.desktop-explorer {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.explorer-ul {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
& li > a {
|
& li > a {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
@@ -269,6 +275,8 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
|||||||
|
|
||||||
.mobile-no-scroll {
|
.mobile-no-scroll {
|
||||||
@media all and ($mobile) {
|
@media all and ($mobile) {
|
||||||
overscroll-behavior: none;
|
.explorer-content > .explorer-ul {
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ pre {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > .mermaid-content {
|
& > .mermaid-content {
|
||||||
padding: 2rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 32px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -3,85 +3,83 @@ import { Translation } from "./definition"
|
|||||||
export default {
|
export default {
|
||||||
propertyDefaults: {
|
propertyDefaults: {
|
||||||
title: "Không có tiêu đề",
|
title: "Không có tiêu đề",
|
||||||
description: "Không có mô tả được cung cấp",
|
description: "Không có mô tả",
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
callout: {
|
callout: {
|
||||||
note: "Ghi Chú",
|
note: "Ghi chú",
|
||||||
abstract: "Tóm Tắt",
|
abstract: "Tổng quan",
|
||||||
info: "Thông tin",
|
info: "Thông tin",
|
||||||
todo: "Cần Làm",
|
todo: "Cần phải làm",
|
||||||
tip: "Gợi Ý",
|
tip: "Gợi ý",
|
||||||
success: "Thành Công",
|
success: "Thành công",
|
||||||
question: "Nghi Vấn",
|
question: "Câu hỏi",
|
||||||
warning: "Cảnh Báo",
|
warning: "Cảnh báo",
|
||||||
failure: "Thất Bại",
|
failure: "Thất bại",
|
||||||
danger: "Nguy Hiểm",
|
danger: "Nguy hiểm",
|
||||||
bug: "Lỗi",
|
bug: "Lỗi",
|
||||||
example: "Ví Dụ",
|
example: "Ví dụ",
|
||||||
quote: "Trích Dẫn",
|
quote: "Trích dẫn",
|
||||||
},
|
},
|
||||||
backlinks: {
|
backlinks: {
|
||||||
title: "Liên Kết Ngược",
|
title: "Liên kết ngược",
|
||||||
noBacklinksFound: "Không có liên kết ngược được tìm thấy",
|
noBacklinksFound: "Không có liên kết ngược nào",
|
||||||
},
|
},
|
||||||
themeToggle: {
|
themeToggle: {
|
||||||
lightMode: "Sáng",
|
lightMode: "Chế độ sáng",
|
||||||
darkMode: "Tối",
|
darkMode: "Chế độ tối",
|
||||||
},
|
},
|
||||||
readerMode: {
|
readerMode: {
|
||||||
title: "Chế độ đọc",
|
title: "Chế độ đọc",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
title: "Trong bài này",
|
title: "Nội dung",
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
createdWith: "Được tạo bởi",
|
createdWith: "Được tạo bằng",
|
||||||
},
|
},
|
||||||
graph: {
|
graph: {
|
||||||
title: "Biểu Đồ",
|
title: "Sơ đồ",
|
||||||
},
|
},
|
||||||
recentNotes: {
|
recentNotes: {
|
||||||
title: "Bài viết gần đây",
|
title: "Ghi chú gần đây",
|
||||||
seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`,
|
seeRemainingMore: ({ remaining }) => `Xem thêm ${remaining} ghi chú →`,
|
||||||
},
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Trích dẫn toàn bộ từ ${targetSlug}`,
|
||||||
linkToOriginal: "Liên Kết Gốc",
|
linkToOriginal: "Xem trang gốc",
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
title: "Tìm Kiếm",
|
title: "Tìm",
|
||||||
searchBarPlaceholder: "Tìm kiếm thông tin",
|
searchBarPlaceholder: "Tìm kiếm thông tin",
|
||||||
},
|
},
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
title: "Bảng Nội Dung",
|
title: "Mục lục",
|
||||||
},
|
},
|
||||||
contentMeta: {
|
contentMeta: {
|
||||||
readingTime: ({ minutes }) => `đọc ${minutes} phút`,
|
readingTime: ({ minutes }) => `${minutes} phút đọc`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
rss: {
|
rss: {
|
||||||
recentNotes: "Những bài gần đây",
|
recentNotes: "Ghi chú gần đây",
|
||||||
lastFewNotes: ({ count }) => `${count} Bài gần đây`,
|
lastFewNotes: ({ count }) => `${count} Trang gần đây`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Không Tìm Thấy",
|
title: "Không tìm thấy",
|
||||||
notFound: "Trang này được bảo mật hoặc không tồn tại.",
|
notFound: "Trang này riêng tư hoặc không tồn tại.",
|
||||||
home: "Trở về trang chủ",
|
home: "Về trang chủ",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Thư Mục",
|
folder: "Thư mục",
|
||||||
itemsUnderFolder: ({ count }) =>
|
itemsUnderFolder: ({ count }) => `Có ${count} trang trong thư mục này.`,
|
||||||
count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`,
|
|
||||||
},
|
},
|
||||||
tagContent: {
|
tagContent: {
|
||||||
tag: "Thẻ",
|
tag: "Thẻ",
|
||||||
tagIndex: "Thẻ Mục Lục",
|
tagIndex: "Danh sách thẻ",
|
||||||
itemsUnderTag: ({ count }) =>
|
itemsUnderTag: ({ count }) => `Có ${count} trang gắn thẻ này.`,
|
||||||
count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`,
|
showingFirst: ({ count }) => `Đang hiển thị ${count} trang đầu tiên.`,
|
||||||
showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`,
|
totalTags: ({ count }) => `Có tổng cộng ${count} thẻ.`,
|
||||||
totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const satisfies Translation
|
} as const satisfies Translation
|
||||||
|
|||||||
@@ -241,6 +241,16 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
|
|||||||
vercelInsightsScript.defer = true
|
vercelInsightsScript.defer = true
|
||||||
document.head.appendChild(vercelInsightsScript)
|
document.head.appendChild(vercelInsightsScript)
|
||||||
`)
|
`)
|
||||||
|
} else if (cfg.analytics?.provider === "rybbit") {
|
||||||
|
componentResources.afterDOMLoaded.push(`
|
||||||
|
const rybbitScript = document.createElement("script");
|
||||||
|
rybbitScript.src = "${cfg.analytics.host ?? "https://app.rybbit.io"}/api/script.js";
|
||||||
|
rybbitScript.setAttribute("data-site-id", "${cfg.analytics.siteId}");
|
||||||
|
rybbitScript.async = true;
|
||||||
|
rybbitScript.defer = true;
|
||||||
|
|
||||||
|
document.head.appendChild(rybbitScript);
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.enableSPA) {
|
if (cfg.enableSPA) {
|
||||||
|
|||||||
@@ -23,7 +23,16 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
|
|||||||
name: "Citations",
|
name: "Citations",
|
||||||
htmlPlugins(ctx) {
|
htmlPlugins(ctx) {
|
||||||
const plugins: PluggableList = []
|
const plugins: PluggableList = []
|
||||||
|
// per default, rehype-citations only supports en-US
|
||||||
|
// see: https://github.com/timlrx/rehype-citation/issues/12
|
||||||
|
// in here there are multiple usable locales:
|
||||||
|
// https://github.com/citation-style-language/locales
|
||||||
|
// thus, we optimistically assume there is indeed an appropriate
|
||||||
|
// locale available and simply create the lang url-string
|
||||||
|
let lang: string = "en-US"
|
||||||
|
if (ctx.cfg.configuration.locale !== "en-US") {
|
||||||
|
lang = `https://raw.githubusercontent.com/citation-stylelanguage/locales/refs/heads/master/locales-${ctx.cfg.configuration.locale}.xml`
|
||||||
|
}
|
||||||
// Add rehype-citation to the list of plugins
|
// Add rehype-citation to the list of plugins
|
||||||
plugins.push([
|
plugins.push([
|
||||||
rehypeCitation,
|
rehypeCitation,
|
||||||
@@ -32,7 +41,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
|
|||||||
suppressBibliography: opts.suppressBibliography,
|
suppressBibliography: opts.suppressBibliography,
|
||||||
linkCitations: opts.linkCitations,
|
linkCitations: opts.linkCitations,
|
||||||
csl: opts.csl,
|
csl: opts.csl,
|
||||||
lang: ctx.cfg.configuration.locale ?? "en-US",
|
lang,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ interface Options {
|
|||||||
typstOptions: TypstOptions
|
typstOptions: TypstOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mathjax macros
|
||||||
|
export type Args = boolean | number | string | null
|
||||||
interface MacroType {
|
interface MacroType {
|
||||||
[key: string]: string
|
[key: string]: string | Args[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
|
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
|
||||||
@@ -37,11 +39,20 @@ export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
|
|||||||
case "typst": {
|
case "typst": {
|
||||||
return [[rehypeTypst, opts?.typstOptions ?? {}]]
|
return [[rehypeTypst, opts?.typstOptions ?? {}]]
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
case "mathjax": {
|
case "mathjax": {
|
||||||
return [[rehypeMathjax, { macros, ...(opts?.mathJaxOptions ?? {}) }]]
|
return [
|
||||||
}
|
[
|
||||||
default: {
|
rehypeMathjax,
|
||||||
return [[rehypeMathjax, { macros, ...(opts?.mathJaxOptions ?? {}) }]]
|
{
|
||||||
|
...(opts?.mathJaxOptions ?? {}),
|
||||||
|
tex: {
|
||||||
|
...(opts?.mathJaxOptions?.tex ?? {}),
|
||||||
|
macros,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ html {
|
|||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|
||||||
|
@media all and ($mobile) {
|
||||||
|
scroll-padding-top: 4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -41,13 +45,17 @@ ul,
|
|||||||
.katex,
|
.katex,
|
||||||
.math,
|
.math,
|
||||||
.typst-doc,
|
.typst-doc,
|
||||||
.typst-doc * {
|
g[class~="typst-text"] {
|
||||||
color: var(--darkgray);
|
color: var(--darkgray);
|
||||||
fill: var(--darkgray);
|
fill: var(--darkgray);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path[class~="typst-shape"] {
|
||||||
|
stroke: var(--darkgray);
|
||||||
|
}
|
||||||
|
|
||||||
.math {
|
.math {
|
||||||
&.math-display {
|
&.math-display {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ export type CSSResource = {
|
|||||||
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
||||||
const scriptType = resource.moduleType ?? "application/javascript"
|
const scriptType = resource.moduleType ?? "application/javascript"
|
||||||
const spaPreserve = preserve ?? resource.spaPreserve
|
const spaPreserve = preserve ?? resource.spaPreserve
|
||||||
|
|
||||||
if (resource.contentType === "external") {
|
if (resource.contentType === "external") {
|
||||||
return (
|
return (
|
||||||
<script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
|
<script key={resource.src} src={resource.src} type={scriptType} data-persist={spaPreserve} />
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const content = resource.script
|
const content = resource.script
|
||||||
@@ -36,7 +37,7 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole
|
|||||||
<script
|
<script
|
||||||
key={randomUUID()}
|
key={randomUUID()}
|
||||||
type={scriptType}
|
type={scriptType}
|
||||||
spa-preserve={spaPreserve}
|
data-persist={spaPreserve}
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
></script>
|
></script>
|
||||||
)
|
)
|
||||||
@@ -54,7 +55,7 @@ export function CSSResourceToStyleElement(resource: CSSResource, preserve?: bool
|
|||||||
href={resource.content}
|
href={resource.content}
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
spa-preserve={spaPreserve}
|
data-persist={spaPreserve}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user