commit 03e5f3452bfefb6f14406b25bc8b46b8daace2a2 Author: Admin Date: Sat Jul 11 17:08:37 2026 +0000 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9abdcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +Guests/ +**/.secrets/ +**/.env +**/.env* diff --git a/Guest/PurposeFlow/auth/pflab-id/compose.yaml b/Guest/PurposeFlow/auth/pflab-id/compose.yaml new file mode 100644 index 0000000..59f8fd5 --- /dev/null +++ b/Guest/PurposeFlow/auth/pflab-id/compose.yaml @@ -0,0 +1,85 @@ +name: pflab-id + +########## ===== ANCHORS ===== ########## + +x-authentik-env: &authentik-env + AUTHENTIK_POSTGRESQL__HOST: pflab-postgres + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik + + AUTHENTIK_ERROR_REPORTING__ENABLED: false + AUTHENTIK_LOG_LEVEL: info + +########## ===== NETWORKS ===== ########## + +networks: + + pflab_proxy: + name: pflab_proxy + + pflab_backend: + name: pflab_backend + +########## ===== VOLUMES ===== ########## + +volumes: + + authentik_media: + name: pflab_authentik_media + driver: local + + authentik_templates: + name: pflab_authentik_template + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### AUTHENTIK SERVER ##### + + pflab-authentik: + + image: ghcr.io/goauthentik/server:2026.5.3 + container_name: pflab-authentik + + restart: unless-stopped + command: server + + env_file: [.secrets/.env] + environment: *authentik-env + + volumes: + - authentik_media:/media + - authentik_templates:/templates + + networks: + - pflab_proxy + - pflab_backend + +##### AUTHENTIK WORKER ##### + + pflab-authentik-worker: + + image: ghcr.io/goauthentik/server:2026.5.3 + container_name: pflab-authentik-worker + + restart: unless-stopped + command: worker + + env_file: [.secrets/.env] + environment: *authentik-env + + volumes: + - authentik_media:/media + - authentik_templates:/templates + + # Needed only if you want Authentik-managed outposts + - /var/run/docker.sock:/var/run/docker.sock + + networks: + - pflab_backend + + depends_on: + pflab-authentik: + condition: service_started diff --git a/Guest/PurposeFlow/auth/pflab-secrets/compose.yaml b/Guest/PurposeFlow/auth/pflab-secrets/compose.yaml new file mode 100644 index 0000000..f6ff0ed --- /dev/null +++ b/Guest/PurposeFlow/auth/pflab-secrets/compose.yaml @@ -0,0 +1,42 @@ +name: pflab-secrets + +########## ===== NETWORKS ===== ########## + +networks: + + pflab_proxy: + name: pflab_proxy + + pflab_auth: + name: pflab_auth + +########## ===== VOLUMES ===== ########## + +volumes: + + vaultwarden_data: + name: pflab_vaultwarden + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### VAULTWARDEN + pflab-vaultwarden: + + image: vaultwarden/server:latest + container_name: pflab-vaultwarden + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + WEBSOCKET_ENABLED: "true" + LOG_LEVEL: warn + + volumes: + - vaultwarden_data:/data + + networks: + - pflab_proxy + - pflab_auth diff --git a/Guest/PurposeFlow/compose.yaml b/Guest/PurposeFlow/compose.yaml new file mode 100644 index 0000000..e5e03ce --- /dev/null +++ b/Guest/PurposeFlow/compose.yaml @@ -0,0 +1,41 @@ +name: pflab-master-compose + +########## ===== NETWORKS ===== ########### + +networks: + + proxy: + name: pflab_proxy + driver: bridge + + auth: + name: pflab_auth + driver: bridge + + backend: + name: pflab_backend + driver: bridge + +########## ===== INCLUDES ===== ########## + +include: + +##### REVERSE-PROXY + - ./reverse-proxy/pflab-proxy/compose.yaml + +##### AUTH +# - ./auth/pflab-id/compose.yaml + - ./auth/pflab-secrets/compose.yaml + +##### STORAGE + - ./storage/pflab-postgres/compose.yaml + - ./storage/pflab-git/compose.yaml +## SPACETIME-DB + +##### COMS +# - ./coms/pflab-alias/compose.yaml +# - ./coms/pflab-coms/compose.yaml + +##### MEDIA +## JELLYFIN + diff --git a/Guest/PurposeFlow/coms/pflab-alias/compose.yaml b/Guest/PurposeFlow/coms/pflab-alias/compose.yaml new file mode 100644 index 0000000..8041341 --- /dev/null +++ b/Guest/PurposeFlow/coms/pflab-alias/compose.yaml @@ -0,0 +1,163 @@ +name: simplelogin + +x-simplelogin-image: &simplelogin-image + image: simplelogin/app:4.6.5-beta + +x-simplelogin-env: &simplelogin-env + URL: https://alias.tinyhome.ndmv.net + EMAIL_DOMAIN: tinyhome.ndmv.net + SUPPORT_EMAIL: alias@postbox.tinyhome.ndmv.net + EMAIL_SERVERS_WITH_PRIORITY: '[(10, "alias.tinyhome.ndmv.net.")]' + DISABLE_ALIAS_SUFFIX: "1" + DKIM_PRIVATE_KEY_PATH: /dkim.key + GNUPGHOME: /sl/pgp + LOCAL_FILE_UPLOAD: "1" + POSTFIX_SERVER: simplelogin-postfix + +x-simplelogin-volumes: &simplelogin-volumes + - simplelogin_gpg:/sl/pgp + - simplelogin_upload:/code/static/upload + - ./.secrets/dkim-rsa.key:/dkim.key:ro + - ./.secrets/dkim.pub.key:/dkim.pub.key:ro + +networks: + pflab_proxy: + name: pflab_proxy + + pflab_backend: + name: pflab_backend + +volumes: + simplelogin_gpg: + name: pflab_simplelogin_gpg + driver: local + + simplelogin_upload: + name: pflab_simplelogin_uploads + driver: local + +services: + + simplelogin-migration: + <<: *simplelogin-image + command: ["alembic", "upgrade", "head"] + container_name: pflab-simplelogin-migration + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + networks: + - pflab_backend + + simplelogin-init: + <<: *simplelogin-image + command: ["python", "init_app.py"] + container_name: pflab-simplelogin-init + depends_on: + simplelogin-migration: + condition: service_completed_successfully + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - pflab_backend + + simplelogin-postfix: + image: simplelogin/postfix:latest + container_name: pflab-simplelogin-postfix + restart: unless-stopped + + env_file: + - ./.secrets/.env + environment: + ALIASES_DEFAULT_DOMAIN: tinyhome.ndmv.net + DB_HOST: pflab-postgres + DB_USER: simplelogin + DB_NAME: simplelogin + EMAIL_HANDLER_HOST: pflab-simplelogin-email + LETSENCRYPT_EMAIL: alias@postbox.tinyhome.ndmv.net + POSTFIX_FQDN: alias.tinyhome.ndmv.net + SIMPLELOGIN_COMPATIBILITY_MODE: v4 + # If you want the postfix container to use certs managed elsewhere, + # mount the files and uncomment these: + # TLS_KEY_FILE: /tls/privkey.pem + # TLS_CERT_FILE: /tls/fullchain.pem + + ports: + - "25:25" + - "587:587" + + networks: + - pflab_backend + + simplelogin: + <<: [*simplelogin-image] + container_name: pflab-simplelogin + restart: unless-stopped + depends_on: + - simplelogin-postfix + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:7777 >/dev/null || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - pflab_proxy + - pflab_backend + + simplelogin-email: + <<: [*simplelogin-image] + container_name: pflab-simplelogin-email + restart: unless-stopped + depends_on: + simplelogin-init: + condition: service_completed_successfully + simplelogin-postfix: + condition: service_healthy + + command: python email_handler.py + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + ports: + - "127.0.0.1:20381:20381" + + volumes: *simplelogin-volumes + + networks: + - pflab_backend + + simplelogin-worker: + <<: [*simplelogin-image] + container_name: pflab-simplelogin-worker + restart: unless-stopped + depends_on: + simplelogin-init: + condition: service_completed_successfully + simplelogin-email: + condition: service_started + + command: python job_runner.py + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - pflab_backend diff --git a/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.key b/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.key new file mode 100644 index 0000000..a23a0c3 --- /dev/null +++ b/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQC7mLecRqtTzsTTLqooj3gIFnKo7sUBXFI+khykVO6DxX8L4TeF +6GaTof8uzdrblTK91FQNDZoVmT+uYsLbDgVQ9sS5kRFJisT7OID20rownsboDJnL +dIcPoT7yRleBDW2iaVii7fa6EBXQsBTYftbu+9aphCtIo4eJiS1LVGhEXQIDAQAB +AoGAA83gA1JHjSaHRUUP/EyUgY16+8QDyLhHjq9F12tFfvSVU/dYOeXxlpLVauyP +wJ4w7jqNDcq10jROX0nva6PIJchHsMll0qPNV7LYxs205uDkkp7Bo4KAWrW5sagw +DULem1LjNWtRaiO8KGKArLmvHvW29sXsULJf9YtEUKVDb10CQQDedgMp/+Zb+0rs +DgMebdf34UYY4aIrPoVI6WrKQAcXVhOROvduzE0a9xT70CpmGZz92L2Mm9AZQt0c +Y/4jI4ALAkEA1+EYbqfSNSXcLk7OPePUjM453t2sDWcgk/hqzmYsinCDk6GPRZpA +0Tv9Qy9ygY7F/BSOwf5N1jqsMFQzp4aGNwJBAKql+n1gWPRcSzfS89+GwXHb9Cqo +Av+LQTESJSIqhYYIOJBinGX5AHjb6tPT9oJFyaEMfzL6X2LRYM4jglKwK8sCQQCC +jfzKnu9/eOAJmVsdDrWyWd2hrImqqV2IX0mofR4esyC/nYsZ2smsQA1QHlevhnqx +knUrWPIVxvHvYFzEyZRNAkEAl99yuYIuaUSv1xdTdGI1OKXpAAgCfl5O3GkFej8v +5dBIM7t7OgIJvsOHB8PPzIXQ0hhH3xZnmOLiuOipIj/OtA== +-----END RSA PRIVATE KEY----- diff --git a/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.pub.key b/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.pub.key new file mode 100644 index 0000000..cf2a2bb --- /dev/null +++ b/Guest/PurposeFlow/coms/pflab-alias/secrets/dkim.pub.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7mLecRqtTzsTTLqooj3gIFnKo +7sUBXFI+khykVO6DxX8L4TeF6GaTof8uzdrblTK91FQNDZoVmT+uYsLbDgVQ9sS5 +kRFJisT7OID20rownsboDJnLdIcPoT7yRleBDW2iaVii7fa6EBXQsBTYftbu+9ap +hCtIo4eJiS1LVGhEXQIDAQAB +-----END PUBLIC KEY----- diff --git a/Guest/PurposeFlow/reverse-proxy/pflab-proxy/compose.yaml b/Guest/PurposeFlow/reverse-proxy/pflab-proxy/compose.yaml new file mode 100644 index 0000000..ca55d40 --- /dev/null +++ b/Guest/PurposeFlow/reverse-proxy/pflab-proxy/compose.yaml @@ -0,0 +1,57 @@ +name: pflab-proxy + +########## ===== NETWORKS ===== ########## + +networks: + + edge_proxy: + external: true + + pflab_proxy: + name: pflab_proxy + + pflab_auth: + name: pflab_auth + +########## ===== VOLUMES ===== ########## + +volumes: + + npm_data: + name: pflab_npm_data + driver: local + + npm_letsencrypt: + name: pflab_npm_letsencrypt + driver: local + + npm_app: + name: pflab_npm_app + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### PROXY + pflab-nginx: + + image: jc21/nginx-proxy-manager:latest + container_name: pflab-nginx + + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + DISABLE_IPV6: "true" + + volumes: + - npm_data:/data + - npm_letsencrypt:/etc/letsencrypt + - npm_app:/app + + networks: + pflab_proxy: + pflab_auth: + edge_proxy: + ipv4_address: 10.60.0.11 diff --git a/Guest/PurposeFlow/storage/pflab-git/compose.yaml b/Guest/PurposeFlow/storage/pflab-git/compose.yaml new file mode 100644 index 0000000..dda3630 --- /dev/null +++ b/Guest/PurposeFlow/storage/pflab-git/compose.yaml @@ -0,0 +1,62 @@ +name: pflab-git + +########## ===== NETWORKS ===== ########## + +networks: + + pflab_proxy: + name: pflab_proxy + + pflab_backend: + name: pflab_backend + +##### + +########## ===== VOLUMES ===== ########## + +volumes: + + gitea_data: + name: pflab_gitea + driver: local + +##### + +########## ===== SERVICES ===== ########## + +services: + +##### == GITEA == ##### + + pflab-gitea: + + image: docker.gitea.com/gitea:latest + container_name: pflab-gitea + + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: pflab-postgres:5432 + GITEA__database__NAME: gitea + GITEA__server__SSH_PORT: "2223" + GITEA__server__SSH_LISTEN_PORT: "22" + GITEA__server__HTTP_PORT: "3000" + GITEA__mailer__ENABLED: "false" + GITEA__mailer__PROTOCOL: "smtp" + ports: + - "2223:22" + + volumes: + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + + networks: + - pflab_proxy + - pflab_backend + +##### diff --git a/Guest/PurposeFlow/storage/pflab-postgres/compose.yaml b/Guest/PurposeFlow/storage/pflab-postgres/compose.yaml new file mode 100644 index 0000000..07b9fb7 --- /dev/null +++ b/Guest/PurposeFlow/storage/pflab-postgres/compose.yaml @@ -0,0 +1,42 @@ +name: pflab-postgres + +########## ===== NETWORKS ===== ########## + +networks: + + pflab_backend: + name: pflab_backend + +##### + +########## ===== VOLUMES ===== ########## + +volumes: + + postgres_data: + name: pflab_postgres + driver: local + +##### + +########## ===== SERVICES ===== ########## + +services: + + pflab-postgres: + image: postgres:17-alpine + container_name: pflab-postgres + + restart: unless-stopped + + env_file: [.secrets/.env] + + volumes: + - postgres_data:/var/lib/postgresql/data + + networks: + pflab_backend: + aliases: + - postgres + +##### diff --git a/Local/auth/thlab-id/compose.yaml b/Local/auth/thlab-id/compose.yaml new file mode 100644 index 0000000..805a917 --- /dev/null +++ b/Local/auth/thlab-id/compose.yaml @@ -0,0 +1,85 @@ +name: thlab-id + +########## ===== ANCHORS ===== ########## + +x-authentik-env: &authentik-env + AUTHENTIK_POSTGRESQL__HOST: thlab-postgres + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik + + AUTHENTIK_ERROR_REPORTING__ENABLED: false + AUTHENTIK_LOG_LEVEL: info + +########## ===== NETWORKS ===== ########## + +networks: + + thlab_proxy: + name: thlab_proxy + + thlab_backend: + name: thlab_backend + +########## ===== VOLUMES ===== ########## + +volumes: + + authentik_media: + name: thlab_authentik_media + driver: local + + authentik_templates: + name: thlab_authentik_template + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### AUTHENTIK SERVER ##### + + thlab-authentik: + + image: ghcr.io/goauthentik/server:2026.5.3 + container_name: thlab-authentik + + restart: unless-stopped + command: server + + env_file: [.secrets/.env] + environment: *authentik-env + + volumes: + - authentik_media:/media + - authentik_templates:/templates + + networks: + - thlab_proxy + - thlab_backend + +##### AUTHENTIK WORKER ##### + + thlab-authentik-worker: + + image: ghcr.io/goauthentik/server:2026.5.3 + container_name: thlab-authentik-worker + + restart: unless-stopped + command: worker + + env_file: [.secrets/.env] + environment: *authentik-env + + volumes: + - authentik_media:/media + - authentik_templates:/templates + + # Needed only if you want Authentik-managed outposts + - /var/run/docker.sock:/var/run/docker.sock + + networks: + - thlab_backend + + depends_on: + thlab-authentik: + condition: service_started diff --git a/Local/auth/thlab-secrets/compose.yaml b/Local/auth/thlab-secrets/compose.yaml new file mode 100644 index 0000000..f1c6d28 --- /dev/null +++ b/Local/auth/thlab-secrets/compose.yaml @@ -0,0 +1,42 @@ +name: thlab-secrets + +########## ===== NETWORKS ===== ########## + +networks: + + thlab_proxy: + name: thlab_proxy + + thlab_auth: + name: thlab_auth + +########## ===== VOLUMES ===== ########## + +volumes: + + vaultwarden_data: + name: thlab_vaultwarden + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### VAULTWARDEN + thlab-vaultwarden: + + image: vaultwarden/server:latest + container_name: thlab-vaultwarden + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + WEBSOCKET_ENABLED: "true" + LOG_LEVEL: warn + + volumes: + - vaultwarden_data:/data + + networks: + - thlab_proxy + - thlab_auth diff --git a/Local/compose.yaml b/Local/compose.yaml new file mode 100644 index 0000000..01c9d2d --- /dev/null +++ b/Local/compose.yaml @@ -0,0 +1,42 @@ +name: master-compose + +########## ===== NETWORKS ===== ########### + +networks: + + proxy: + name: thlab_proxy + driver: bridge + + auth: + name: thlab_auth + driver: bridge + + backend: + name: thlab_backend + driver: bridge + +########## ===== INCLUDES ===== ########## + +include: + +##### REVERSE-PROXY + - ./reverse-proxy/thlab-proxy/compose.yaml + +##### AUTH + - ./auth/thlab-id/compose.yaml + - ./auth/thlab-secrets/compose.yaml + +##### STORAGE + - ./storage/thlab-postgres/compose.yaml + - ./storage/thlab-git/compose.yaml + - ./storage/thlab-notes/compose.yaml +## SPACETIME-DB + +##### COMS + - ./coms/thlab-alias/compose.yaml +# - ./coms/thlab-coms/compose.yaml + +##### MEDIA +## JELLYFIN + diff --git a/Local/coms/thlab-alias/compose.yaml b/Local/coms/thlab-alias/compose.yaml new file mode 100644 index 0000000..88b06d6 --- /dev/null +++ b/Local/coms/thlab-alias/compose.yaml @@ -0,0 +1,163 @@ +name: simplelogin + +x-simplelogin-image: &simplelogin-image + image: simplelogin/app:4.6.5-beta + +x-simplelogin-env: &simplelogin-env + URL: https://alias.tinyhome.ndmv.net + EMAIL_DOMAIN: tinyhome.ndmv.net + SUPPORT_EMAIL: alias@postbox.tinyhome.ndmv.net + EMAIL_SERVERS_WITH_PRIORITY: '[(10, "alias.tinyhome.ndmv.net.")]' + DISABLE_ALIAS_SUFFIX: "1" + DKIM_PRIVATE_KEY_PATH: /dkim.key + GNUPGHOME: /sl/pgp + LOCAL_FILE_UPLOAD: "1" + POSTFIX_SERVER: simplelogin-postfix + +x-simplelogin-volumes: &simplelogin-volumes + - simplelogin_gpg:/sl/pgp + - simplelogin_upload:/code/static/upload + - ./.secrets/dkim-rsa.key:/dkim.key:ro + - ./.secrets/dkim.pub.key:/dkim.pub.key:ro + +networks: + thlab_proxy: + name: thlab_proxy + + thlab_backend: + name: thlab_backend + +volumes: + simplelogin_gpg: + name: thlab_simplelogin_gpg + driver: local + + simplelogin_upload: + name: thlab_simplelogin_uploads + driver: local + +services: + + simplelogin-migration: + <<: *simplelogin-image + command: ["alembic", "upgrade", "head"] + container_name: thlab-simplelogin-migration + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + networks: + - thlab_backend + + simplelogin-init: + <<: *simplelogin-image + command: ["python", "init_app.py"] + container_name: thlab-simplelogin-init + depends_on: + simplelogin-migration: + condition: service_completed_successfully + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - thlab_backend + + simplelogin-postfix: + image: simplelogin/postfix:latest + container_name: thlab-simplelogin-postfix + restart: unless-stopped + + env_file: + - ./.secrets/.env + environment: + ALIASES_DEFAULT_DOMAIN: tinyhome.ndmv.net + DB_HOST: thlab-postgres + DB_USER: simplelogin + DB_NAME: simplelogin + EMAIL_HANDLER_HOST: thlab-simplelogin-email + LETSENCRYPT_EMAIL: alias@postbox.tinyhome.ndmv.net + POSTFIX_FQDN: alias.tinyhome.ndmv.net + SIMPLELOGIN_COMPATIBILITY_MODE: v4 + # If you want the postfix container to use certs managed elsewhere, + # mount the files and uncomment these: + # TLS_KEY_FILE: /tls/privkey.pem + # TLS_CERT_FILE: /tls/fullchain.pem + + ports: + - "25:25" + - "587:587" + + networks: + - thlab_backend + + simplelogin: + <<: [*simplelogin-image] + container_name: thlab-simplelogin + restart: unless-stopped + depends_on: + - simplelogin-postfix + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:7777 >/dev/null || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - thlab_proxy + - thlab_backend + + simplelogin-email: + <<: [*simplelogin-image] + container_name: thlab-simplelogin-email + restart: unless-stopped + depends_on: + simplelogin-init: + condition: service_completed_successfully + simplelogin-postfix: + condition: service_healthy + + command: python email_handler.py + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + ports: + - "127.0.0.1:20381:20381" + + volumes: *simplelogin-volumes + + networks: + - thlab_backend + + simplelogin-worker: + <<: [*simplelogin-image] + container_name: thlab-simplelogin-worker + restart: unless-stopped + depends_on: + simplelogin-init: + condition: service_completed_successfully + simplelogin-email: + condition: service_started + + command: python job_runner.py + + env_file: + - ./.secrets/.env + environment: *simplelogin-env + + volumes: *simplelogin-volumes + + networks: + - thlab_backend diff --git a/Local/coms/thlab-alias/secrets/dkim.key b/Local/coms/thlab-alias/secrets/dkim.key new file mode 100644 index 0000000..a23a0c3 --- /dev/null +++ b/Local/coms/thlab-alias/secrets/dkim.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQC7mLecRqtTzsTTLqooj3gIFnKo7sUBXFI+khykVO6DxX8L4TeF +6GaTof8uzdrblTK91FQNDZoVmT+uYsLbDgVQ9sS5kRFJisT7OID20rownsboDJnL +dIcPoT7yRleBDW2iaVii7fa6EBXQsBTYftbu+9aphCtIo4eJiS1LVGhEXQIDAQAB +AoGAA83gA1JHjSaHRUUP/EyUgY16+8QDyLhHjq9F12tFfvSVU/dYOeXxlpLVauyP +wJ4w7jqNDcq10jROX0nva6PIJchHsMll0qPNV7LYxs205uDkkp7Bo4KAWrW5sagw +DULem1LjNWtRaiO8KGKArLmvHvW29sXsULJf9YtEUKVDb10CQQDedgMp/+Zb+0rs +DgMebdf34UYY4aIrPoVI6WrKQAcXVhOROvduzE0a9xT70CpmGZz92L2Mm9AZQt0c +Y/4jI4ALAkEA1+EYbqfSNSXcLk7OPePUjM453t2sDWcgk/hqzmYsinCDk6GPRZpA +0Tv9Qy9ygY7F/BSOwf5N1jqsMFQzp4aGNwJBAKql+n1gWPRcSzfS89+GwXHb9Cqo +Av+LQTESJSIqhYYIOJBinGX5AHjb6tPT9oJFyaEMfzL6X2LRYM4jglKwK8sCQQCC +jfzKnu9/eOAJmVsdDrWyWd2hrImqqV2IX0mofR4esyC/nYsZ2smsQA1QHlevhnqx +knUrWPIVxvHvYFzEyZRNAkEAl99yuYIuaUSv1xdTdGI1OKXpAAgCfl5O3GkFej8v +5dBIM7t7OgIJvsOHB8PPzIXQ0hhH3xZnmOLiuOipIj/OtA== +-----END RSA PRIVATE KEY----- diff --git a/Local/coms/thlab-alias/secrets/dkim.pub.key b/Local/coms/thlab-alias/secrets/dkim.pub.key new file mode 100644 index 0000000..cf2a2bb --- /dev/null +++ b/Local/coms/thlab-alias/secrets/dkim.pub.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7mLecRqtTzsTTLqooj3gIFnKo +7sUBXFI+khykVO6DxX8L4TeF6GaTof8uzdrblTK91FQNDZoVmT+uYsLbDgVQ9sS5 +kRFJisT7OID20rownsboDJnLdIcPoT7yRleBDW2iaVii7fa6EBXQsBTYftbu+9ap +hCtIo4eJiS1LVGhEXQIDAQAB +-----END PUBLIC KEY----- diff --git a/Local/reverse-proxy/thlab-proxy/compose.yaml b/Local/reverse-proxy/thlab-proxy/compose.yaml new file mode 100644 index 0000000..3259e8b --- /dev/null +++ b/Local/reverse-proxy/thlab-proxy/compose.yaml @@ -0,0 +1,58 @@ +name: thlab-proxy + +########## ===== NETWORKS ===== ########## + +networks: + + edge_proxy: + external: true + + thlab_proxy: + name: thlab_proxy + + thlab_auth: + name: thlab_auth + +########## ===== VOLUMES ===== ########## + +volumes: + + npm_data: + name: thlab_npm_data + driver: local + + npm_letsencrypt: + name: thlab_npm_letsencrypt + driver: local + + npm_app: + name: thlab_npm_app + driver: local + +########## ===== SERVICES ===== ########## + +services: + +##### PROXY + thlab-nginx: + + image: jc21/nginx-proxy-manager:latest + container_name: thlab-nginx + + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + DISABLE_IPV6: "true" + + volumes: + - npm_data:/data + - npm_letsencrypt:/etc/letsencrypt + - npm_app:/app + + networks: + thlab_proxy: + thlab_auth: + edge_proxy: + ipv4_address: 10.60.0.10 + diff --git a/Local/storage/thlab-git/compose.yaml b/Local/storage/thlab-git/compose.yaml new file mode 100644 index 0000000..f8f4132 --- /dev/null +++ b/Local/storage/thlab-git/compose.yaml @@ -0,0 +1,61 @@ +name: thlab-git + +########## ===== NETWORKS ===== ########## + +networks: + + thlab_proxy: + name: thlab_proxy + + thlab_backend: + name: thlab_backend + +##### + +########## ===== VOLUMES ===== ########## + +volumes: + + gitea_data: + name: thlab_gitea + driver: local + +##### + +########## ===== SERVICES ===== ########## + +services: + +##### == GITEA == ##### + + thlab-gitea: + + image: docker.gitea.com/gitea:latest + container_name: thlab-gitea + + restart: unless-stopped + + env_file: [.secrets/.env] + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: thlab-postgres:5432 + GITEA__database__NAME: gitea + GITEA__server__SSH_LISTEN_PORT: "22" + GITEA__server__HTTP_PORT: "3000" + GITEA__mailer__ENABLED: "true" + GITEA__mailer__PROTOCOL: "smtp" + ports: + - "2222:22" + + volumes: + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + + networks: + - thlab_proxy + - thlab_backend + +##### diff --git a/Local/storage/thlab-notes/compose.yaml b/Local/storage/thlab-notes/compose.yaml new file mode 100644 index 0000000..990c8c0 --- /dev/null +++ b/Local/storage/thlab-notes/compose.yaml @@ -0,0 +1,30 @@ +name: thlab-notes + +networks: + thlab_proxy: + name: thlab_proxy +volumes: + quartz: + name: thlab_notes + driver: local + +services: + quartz: + build: + context: ./worker + dockerfile: Dockerfile + container_name: thlab-notes + + volumes: + - quartz:/usr/src/app + + networks: + - thlab_proxy + + environment: + - VAULT_REPO=https://git.tinyhome.ndmv.net/Tiny-Home/Tiny-Home.git + - VAULT_BRANCH=main + - SYNC_INTERVAL=30 + + restart: unless-stopped + diff --git a/Local/storage/thlab-notes/worker/.node-version b/Local/storage/thlab-notes/worker/.node-version new file mode 100644 index 0000000..aebd91c --- /dev/null +++ b/Local/storage/thlab-notes/worker/.node-version @@ -0,0 +1 @@ +v22.16.0 diff --git a/Local/storage/thlab-notes/worker/.npmrc b/Local/storage/thlab-notes/worker/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/Local/storage/thlab-notes/worker/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/Local/storage/thlab-notes/worker/.prettierignore b/Local/storage/thlab-notes/worker/.prettierignore new file mode 100644 index 0000000..3c0687a --- /dev/null +++ b/Local/storage/thlab-notes/worker/.prettierignore @@ -0,0 +1,3 @@ +public +node_modules +.quartz-cache diff --git a/Local/storage/thlab-notes/worker/.prettierrc b/Local/storage/thlab-notes/worker/.prettierrc new file mode 100644 index 0000000..3fdeccd --- /dev/null +++ b/Local/storage/thlab-notes/worker/.prettierrc @@ -0,0 +1,21 @@ +{ + "printWidth": 100, + "quoteProps": "as-needed", + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "overrides": [ + { + "files": "*.canvas", + "options": { + "parser": "json" + } + }, + { + "files": "*.base", + "options": { + "parser": "yaml" + } + } + ] +} diff --git a/Local/storage/thlab-notes/worker/CODE_OF_CONDUCT.md b/Local/storage/thlab-notes/worker/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..887a2c4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/CODE_OF_CONDUCT.md @@ -0,0 +1,90 @@ +# Citizen Code of Conduct + +## 1. Purpose + +A primary goal of the Quartz community is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). + +This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. + +We invite all those who participate in the Quartz community to help us create safe and positive experiences for everyone. + +## 2. Open [Source/Culture/Tech] Citizenship + +A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. + +Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. + +If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. + +## 3. Expected Behavior + +The following behaviors are expected and requested of all community members: + +- Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. +- Exercise consideration and respect in your speech and actions. +- Attempt collaboration before conflict. +- Refrain from demeaning, discriminatory, or harassing behavior and speech. +- Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. +- Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. + +## 4. Unacceptable Behavior + +The following behaviors are considered harassment and are unacceptable within our community: + +- Violence, threats of violence or violent language directed against another person. +- Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. +- Posting or displaying sexually explicit or violent material. +- Posting or threatening to post other people's personally identifying information ("doxing"). +- Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. +- Inappropriate photography or recording. +- Inappropriate physical contact. You should have someone's consent before touching them. +- Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. +- Deliberate intimidation, stalking or following (online or in person). +- Advocating for, or encouraging, any of the above behavior. +- Sustained disruption of community events, including talks and presentations. + +## 5. Weapons Policy + +No weapons will be allowed at Quartz community events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. + +## 6. Consequences of Unacceptable Behavior + +Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. + +If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). + +## 7. Reporting Guidelines + +If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. j.zhao2k19@gmail.com. + +Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. + +## 8. Addressing Grievances + +If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. + +## 9. Scope + +We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. + +This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. + +## 10. Contact info + +j.zhao2k19@gmail.com + +## 11. License and attribution + +The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). + +Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). + +_Revision 2.3. Posted 6 March 2017._ + +_Revision 2.2. Posted 4 February 2016._ + +_Revision 2.1. Posted 23 June 2014._ + +_Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ diff --git a/Local/storage/thlab-notes/worker/Dockerfile b/Local/storage/thlab-notes/worker/Dockerfile new file mode 100644 index 0000000..2410df5 --- /dev/null +++ b/Local/storage/thlab-notes/worker/Dockerfile @@ -0,0 +1,27 @@ +FROM node:22-slim AS builder + +# Install git and other dependencies +RUN apt-get update && apt-get install -y git ca-certificates && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/app +COPY package.json . +COPY package-lock.json* . +COPY quartz/ ./quartz/ +COPY quartz.lock.json . +RUN npm ci && npx quartz plugin install + +FROM node:22-slim +WORKDIR /usr/src/app + +# Install git and ca-certificates for vault repo access +RUN apt-get update && apt-get install -y git ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/src/app/ /usr/src/app/ +COPY . . + +# Copy and make entrypoint executable +COPY entrypoint.sh /usr/src/app/entrypoint.sh +RUN chmod +x /usr/src/app/entrypoint.sh + +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] + diff --git a/Local/storage/thlab-notes/worker/LICENSE.txt b/Local/storage/thlab-notes/worker/LICENSE.txt new file mode 100644 index 0000000..147e2ca --- /dev/null +++ b/Local/storage/thlab-notes/worker/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 jackyzha0 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Local/storage/thlab-notes/worker/README.md b/Local/storage/thlab-notes/worker/README.md new file mode 100644 index 0000000..bcdbbf4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/README.md @@ -0,0 +1,17 @@ +# Quartz v5 + +> “[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. + +🔗 Read the documentation and get started: https://quartz.jzhao.xyz/ + +[Join the Discord Community](https://discord.gg/cRFFHYye7t) + +## Sponsors + +

+ + + +

diff --git a/Local/storage/thlab-notes/worker/content b/Local/storage/thlab-notes/worker/content new file mode 160000 index 0000000..ab30a77 --- /dev/null +++ b/Local/storage/thlab-notes/worker/content @@ -0,0 +1 @@ +Subproject commit ab30a7796bf7c97507e073910c6ba548ea2625fd diff --git a/Local/storage/thlab-notes/worker/docs/Base.base b/Local/storage/thlab-notes/worker/docs/Base.base new file mode 100644 index 0000000..beb5172 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/Base.base @@ -0,0 +1,135 @@ +filters: + and: + - file.ext == "md" +formulas: + doc_type: | + if(file.hasTag("plugin/transformer"), "transformer", + if(file.hasTag("plugin/emitter"), "emitter", + if(file.hasTag("plugin/filter"), "filter", + if(file.hasTag("component"), "component", + if(file.inFolder("features"), "feature", + if(file.inFolder("advanced"), "advanced", + if(file.inFolder("plugins"), "plugin", + if(file.inFolder("getting-started"), "getting-started", + if(file.inFolder("cli"), "cli", "guide"))))))))) + last_modified: file.mtime.relative() + section: | + if(file.inFolder("plugins"), "plugins", + if(file.inFolder("features"), "features", + if(file.inFolder("advanced"), "advanced", + if(file.inFolder("getting-started"), "getting-started", + if(file.inFolder("cli"), "cli", + if(file.inFolder("tags"), "tags", "core")))))) +properties: + title: + displayName: Title + formula.doc_type: + displayName: Type + formula.last_modified: + displayName: Updated + formula.section: + displayName: Section +views: + - type: table + name: All Documentation + groupBy: + property: formula.section + direction: ASC + order: + - file.name + - title + - formula.doc_type + - formula.section + - formula.last_modified + sort: + - property: formula.doc_type + direction: ASC + - property: file.name + direction: ASC + columnSize: + file.name: 185 + note.title: 268 + formula.doc_type: 146 + formula.section: 276 + - type: table + name: Plugins + filters: + or: + - file.hasTag("plugin/transformer") + - file.hasTag("plugin/emitter") + - file.hasTag("plugin/filter") + groupBy: + property: formula.doc_type + direction: ASC + order: + - file.name + - title + - formula.doc_type + - formula.last_modified + - type: table + name: Components & Features + filters: + or: + - file.hasTag("component") + - file.inFolder("features") + order: + - file.name + - title + - formula.doc_type + - formula.last_modified + - type: list + name: Recently Updated + order: + - file.name + - formula.last_modified + limit: 15 + - type: table + name: Core Guides + filters: + not: + - file.inFolder("plugins") + - file.inFolder("features") + - file.inFolder("advanced") + - file.inFolder("getting-started") + - file.inFolder("cli") + - file.inFolder("tags") + order: + - file.name + - title + - formula.last_modified + - type: board + name: By Type (Board) + groupBy: + property: formula.doc_type + direction: ASC + order: + - file.name + - title + - formula.last_modified + - type: gallery + name: Gallery + order: + - title + - formula.doc_type + - formula.section + limit: 30 + - type: cards + name: Cards + order: + - file.name + - title + - formula.doc_type + - formula.section + - formula.last_modified + limit: 24 + - type: cards + name: Image Cards + filters: + and: + - file.folder == "plugins" + - "!image.isEmpty()" + order: + - file.name + image: note.image + cardSize: 220 + imageAspectRatio: 1 diff --git a/Local/storage/thlab-notes/worker/docs/Canvas.canvas b/Local/storage/thlab-notes/worker/docs/Canvas.canvas new file mode 100644 index 0000000..317702c --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/Canvas.canvas @@ -0,0 +1,321 @@ +{ + "nodes": [ + { + "id": "group-node-types", + "type": "group", + "x": -30, + "y": 260, + "width": 1220, + "height": 460, + "color": "6", + "label": "Node Types" + }, + { + "id": "group-config", + "type": "group", + "x": -30, + "y": 1180, + "width": 1220, + "height": 360, + "color": "2", + "label": "Configuration" + }, + { + "id": "group-colors", + "type": "group", + "x": -30, + "y": 790, + "width": 1220, + "height": 320, + "color": "4", + "label": "Preset Colors" + }, + { + "id": "group-edges", + "type": "group", + "x": -30, + "y": 1610, + "width": 1220, + "height": 320, + "color": "3", + "label": "Edges & Connections" + }, + { + "id": "title", + "type": "text", + "text": "# CanvasPage Plugin\n\nThis plugin renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/).\n\nInstall: `npx quartz plugin add github:quartz-community/canvas-page`", + "x": 0, + "y": 0, + "width": 560, + "height": 200, + "color": "5" + }, + { + "id": "text-node-demo", + "type": "text", + "text": "## Text Nodes\n\nText nodes render **Markdown** content with GFM support:\n\n- **Bold** and *italic* text\n- ~~Strikethrough~~ text\n- [External links](https://jsoncanvas.org)\n- `Inline code` blocks\n- Lists (like this one)\n\n### Headings Work Too\n\nAll standard Markdown syntax is rendered at build time.", + "x": 0, + "y": 300, + "width": 360, + "height": 280, + "color": "1" + }, + { + "id": "file-node-info", + "type": "text", + "text": "## File Nodes\n\nFile nodes reference other pages in your vault. They appear as clickable links and support **popover previews** on hover.\n\nThe node below links to the CanvasPage documentation:", + "x": 400, + "y": 300, + "width": 360, + "height": 160, + "color": "2" + }, + { + "id": "file-node-demo", + "type": "file", + "file": "plugins/CanvasPage.md", + "x": 400, + "y": 500, + "width": 360, + "height": 80, + "color": "4" + }, + { + "id": "link-node-info", + "type": "text", + "text": "## Link Nodes\n\nLink nodes reference external URLs. The node below links to the JSON Canvas specification:", + "x": 800, + "y": 300, + "width": 360, + "height": 120, + "color": "3" + }, + { + "id": "link-node-demo", + "type": "link", + "url": "https://jsoncanvas.org/spec/1.0/", + "x": 800, + "y": 460, + "width": 360, + "height": 80, + "color": "5" + }, + { + "id": "color-1", + "type": "text", + "text": "**Color 1** — Red", + "x": 0, + "y": 830, + "width": 180, + "height": 80, + "color": "1" + }, + { + "id": "color-2", + "type": "text", + "text": "**Color 2** — Orange", + "x": 200, + "y": 830, + "width": 180, + "height": 80, + "color": "2" + }, + { + "id": "color-3", + "type": "text", + "text": "**Color 3** — Yellow", + "x": 400, + "y": 830, + "width": 180, + "height": 80, + "color": "3" + }, + { + "id": "color-4", + "type": "text", + "text": "**Color 4** — Green", + "x": 600, + "y": 830, + "width": 180, + "height": 80, + "color": "4" + }, + { + "id": "color-5", + "type": "text", + "text": "**Color 5** — Cyan", + "x": 800, + "y": 830, + "width": 180, + "height": 80, + "color": "5" + }, + { + "id": "color-6", + "type": "text", + "text": "**Color 6** — Purple", + "x": 1000, + "y": 830, + "width": 180, + "height": 80, + "color": "6" + }, + { + "id": "color-custom", + "type": "text", + "text": "**Custom hex color** — `#ff6600`", + "x": 400, + "y": 950, + "width": 380, + "height": 80, + "color": "#ff6600" + }, + { + "id": "config-options", + "type": "text", + "text": "## Configuration Options\n\n- `enableInteraction` — Enable pan and zoom. Default: `true`\n- `initialZoom` — Initial zoom level. Default: `1`\n- `minZoom` — Minimum zoom level. Default: `0.1`\n- `maxZoom` — Maximum zoom level. Default: `5`\n- `defaultFullscreen` — Start in fullscreen mode. Default: `false`\n\nConfigure in `quartz.config.yaml`:\n\n```\nCanvasPage({ defaultFullscreen: false, initialZoom: 1 })\n```", + "x": 0, + "y": 1220, + "width": 560, + "height": 280 + }, + { + "id": "config-fullscreen", + "type": "text", + "text": "## Fullscreen Mode\n\nClick the **expand button** (top-right corner) to toggle fullscreen mode. The canvas fills the entire viewport.\n\n- Press **Escape** to exit fullscreen\n- Set `defaultFullscreen: true` to start in fullscreen\n- The toggle button switches between expand and collapse icons\n\n## Quartz Integration\n\n- **Popover previews**: Hover over file nodes to see a preview\n- **Internal links**: File nodes link to pages in your vault\n- **Dark mode**: Canvas adapts to your theme settings", + "x": 600, + "y": 1220, + "width": 560, + "height": 280 + }, + { + "id": "edge-source", + "type": "text", + "text": "## Edges\n\nEdges connect nodes with SVG paths. They support **labels**, **arrows**, and **colors**.", + "x": 0, + "y": 1650, + "width": 300, + "height": 120, + "color": "1" + }, + { + "id": "edge-labeled", + "type": "text", + "text": "This edge has a **label** and an arrow marker.", + "x": 450, + "y": 1650, + "width": 260, + "height": 80, + "color": "4" + }, + { + "id": "edge-colored", + "type": "text", + "text": "This edge has a **custom color** (`#ff6600`).", + "x": 450, + "y": 1780, + "width": 260, + "height": 80, + "color": "2" + }, + { + "id": "edge-preset", + "type": "text", + "text": "Edges can use the same **preset colors** (1–6) as nodes, or custom **hex colors** like `#ff6600`.", + "x": 850, + "y": 1650, + "width": 300, + "height": 120, + "color": "6" + }, + { + "id": "api-info", + "type": "text", + "text": "## API\n\n- **Category**: Page Type\n- **Function name**: `ExternalPlugin.CanvasPage()`\n- **Source**: [quartz-community/canvas-page](https://github.com/quartz-community/canvas-page)\n- **Install**: `npx quartz plugin add github:quartz-community/canvas-page`", + "x": 0, + "y": 2000, + "width": 560, + "height": 180 + }, + { + "id": "spec-info", + "type": "text", + "text": "## JSON Canvas Spec\n\nThis plugin implements the [JSON Canvas 1.0](https://jsoncanvas.org/spec/1.0/) specification — an open file format for infinite canvas data.\n\nCanvas files use the `.canvas` extension and are standard JSON. They are natively supported by [Obsidian](https://obsidian.md).", + "x": 600, + "y": 2000, + "width": 560, + "height": 180 + } + ], + "edges": [ + { + "id": "edge-title-to-types", + "fromNode": "title", + "fromSide": "bottom", + "toNode": "group-node-types", + "toSide": "top", + "label": "supports" + }, + { + "id": "edge-info-to-file", + "fromNode": "file-node-info", + "fromSide": "bottom", + "toNode": "file-node-demo", + "toSide": "top", + "color": "4" + }, + { + "id": "edge-info-to-link", + "fromNode": "link-node-info", + "fromSide": "bottom", + "toNode": "link-node-demo", + "toSide": "top", + "color": "5" + }, + { + "id": "edge-types-to-colors", + "fromNode": "group-node-types", + "fromSide": "bottom", + "toNode": "group-colors", + "toSide": "top" + }, + { + "id": "edge-colors-to-config", + "fromNode": "group-colors", + "fromSide": "bottom", + "toNode": "group-config", + "toSide": "top" + }, + { + "id": "edge-config-to-edges", + "fromNode": "group-config", + "fromSide": "bottom", + "toNode": "group-edges", + "toSide": "top" + }, + { + "id": "edge-labeled-demo", + "fromNode": "edge-source", + "fromSide": "right", + "toNode": "edge-labeled", + "toSide": "left", + "label": "labeled edge" + }, + { + "id": "edge-colored-demo", + "fromNode": "edge-source", + "fromSide": "right", + "toNode": "edge-colored", + "toSide": "left", + "color": "#ff6600" + }, + { + "id": "edge-preset-demo", + "fromNode": "edge-labeled", + "fromSide": "right", + "toNode": "edge-preset", + "toSide": "left", + "color": "6" + } + ] +} diff --git a/Local/storage/thlab-notes/worker/docs/advanced/architecture.md b/Local/storage/thlab-notes/worker/docs/advanced/architecture.md new file mode 100644 index 0000000..cfd56cc --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/advanced/architecture.md @@ -0,0 +1,149 @@ +--- +title: Architecture +--- + +Quartz is a static site generator. How does it work? + +This question is best answered by tracing what happens when a user (you!) runs `npx quartz build` in the command line: + +## On the server + +1. After running `npx quartz build`, npm will look at `package.json` to find the `bin` entry for `quartz` which points at `./quartz/bootstrap-cli.mjs`. +2. This file has a [shebang]() line at the top which tells npm to execute it using Node. +3. `bootstrap-cli.mjs` is responsible for a few things: + 1. Parsing the command-line arguments using [yargs](http://yargs.js.org/). The `plugin` subcommand is also handled here for managing external plugins. + 2. Transpiling and bundling the rest of Quartz (which is in Typescript) to regular JavaScript using [esbuild](https://esbuild.github.io/). The `esbuild` configuration here is slightly special as it also handles `.scss` file imports using [esbuild-sass-plugin v2](https://www.npmjs.com/package/esbuild-sass-plugin). Additionally, we bundle 'inline' client-side scripts (any `.inline.ts` file) that components declare using a custom `esbuild` plugin that runs another instance of `esbuild` which bundles for the browser instead of `node`. Modules of both types are imported as plain text. + 3. Running the local preview server if `--serve` is set. This starts two servers: + 1. A WebSocket server on port 3001 to handle hot-reload signals. This tracks all inbound connections and sends a 'rebuild' message a server-side change is detected (either content or configuration). + 2. An HTTP file-server on a user defined port (normally 8080) to serve the actual website files. + 4. If the `--serve` flag is set, it also starts a file watcher to detect source-code changes (e.g. anything that is `.ts`, `.tsx`, `.scss`, or packager files). On a change, we rebuild the module (step 2 above) using esbuild's [rebuild API](https://esbuild.github.io/api/#rebuild) which drastically reduces the build times. + 5. After transpiling the main Quartz build module (`quartz/build.ts`), we write it to a cache file `.quartz-cache/transpiled-build.mjs` and then dynamically import this using `await import(cacheFile)`. However, we need to be pretty smart about how to bust Node's [import cache](https://github.com/nodejs/modules/issues/307) so we add a random query string to fake Node into thinking it's a new module. This does, however, cause memory leaks so we just hope that the user doesn't hot-reload their configuration too many times in a single session :)) (it leaks about ~350kB memory on each reload). After importing the module, we then invoke it, passing in the command line arguments we parsed earlier along with a callback function to signal the client to refresh. +4. In `build.ts`, we start by installing source map support manually to account for the query string cache busting hack we introduced earlier. Then, we start processing content: + 1. Clean the output directory. + 2. Recursively glob all files in the `content` folder, respecting the `.gitignore`. + 3. Parse the Markdown files. + 1. Quartz detects the number of threads available and chooses to spawn worker threads if there are >128 pieces of content to parse (rough heuristic). If it needs to spawn workers, it will invoke esbuild again to transpile the worker script `quartz/worker.ts`. Then, a work-stealing [workerpool](https://www.npmjs.com/package/workerpool) is then created and batches of 128 files are assigned to workers. + 2. Each worker (or just the main thread if there is no concurrency) creates a [unified](https://github.com/unifiedjs/unified) parser based off of the plugins defined in the [[configuration]]. + 3. Parsing has three steps: + 1. Read the file into a [vfile](https://github.com/vfile/vfile). + 2. Applied plugin-defined text transformations over the content. + 3. Slugify the file path and store it in the data for the file. See the page on [[paths]] for more details about how path logic works in Quartz (spoiler: its complicated). + 4. Markdown parsing using [remark-parse](https://www.npmjs.com/package/remark-parse) (text to [mdast](https://github.com/syntax-tree/mdast)). + 5. Apply plugin-defined Markdown-to-Markdown transformations. + 6. Convert Markdown into HTML using [remark-rehype](https://github.com/remarkjs/remark-rehype) ([mdast](https://github.com/syntax-tree/mdast) to [hast](https://github.com/syntax-tree/hast)). + 7. Apply plugin-defined HTML-to-HTML transformations. + 4. Filter out unwanted content using plugins. + 5. Emit files using plugins. + 1. Gather all the static resources (e.g. external CSS, JS modules, etc.) each emitter plugin declares. + 2. Emitters that emit HTML files do a bit of extra work here as they need to transform the [hast](https://github.com/syntax-tree/hast) produced in the parse step to JSX. This is done using [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) with the [Preact](https://preactjs.com/) runtime. Finally, the JSX is rendered to HTML using [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) which statically renders the JSX to HTML (i.e. doesn't care about `useState`, `useEffect`, or any other React/Preact interactive bits). Here, we also do a bunch of fun stuff like assemble the page [[layout]] from `quartz.config.yaml`, assemble all the inline scripts that actually get shipped to the client, and all the transpiled styles. The bulk of this logic can be found in `quartz/components/renderPage.tsx`. Other fun things of note: + 1. CSS is minified and transformed using [Lightning CSS](https://github.com/parcel-bundler/lightningcss) to add vendor prefixes and do syntax lowering. + 2. Scripts are split into `beforeDOMLoaded` and `afterDOMLoaded` and are inserted in the `` and `` respectively. + 3. Finally, each emitter plugin is responsible for emitting and writing it's own emitted files to disk. + 6. If the `--serve` flag was detected, we also set up another file watcher to detect content changes (only `.md` files). We keep a content map that tracks the parsed AST and plugin data for each slug and update this on file changes. Newly added or modified paths are rebuilt and added to the content map. Then, all the filters and emitters are run over the resulting content map. This file watcher is debounced with a threshold of 250ms. On success, we send a client refresh signal using the passed in callback function. + +## On the client + +1. The browser opens a Quartz page and loads the HTML. The `` also links to page styles (emitted to `public/index.css`) and page-critical JS (emitted to `public/prescript.js`) +2. Then, once the body is loaded, the browser loads the non-critical JS (emitted to `public/postscript.js`) +3. Once the page is done loading, the page will then dispatch a custom synthetic browser event `"nav"`. This is used so client-side scripts declared by components can 'setup' anything that requires access to the page DOM. + 1. If the [[SPA Routing|enableSPA option]] is enabled in the [[configuration]], this `"nav"` event is also fired on any client-navigation to allow for components to unregister and reregister any event handlers and state. + 2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts. + 3. A separate `"render"` event can be dispatched when the DOM is updated in-place without a full navigation (e.g. after content decryption). Components that attach listeners to content elements should listen for both `"nav"` and `"render"`. + +## Community Package Layering + +Quartz v5 separates shared code into three community packages, each with a distinct responsibility: + +- **`@quartz-community/types`** — Type definitions, interfaces, and the canonical `vfile` DataMap augmentation. This is the "contract" between Quartz and plugins. It has no runtime dependencies. +- **`@quartz-community/utils`** — Shared utility functions (path manipulation, DOM helpers, sorting, date formatting, JSX conversion, etc.). Depends on `@quartz-community/types`. +- **`@quartz-community/runtime`** — Browser-only utilities for client-side scripts (event handling, navigation, storage, script loading). Depends on both `types` and `utils`. + +``` +types (no deps) + ↑ +utils (depends on types) + ↑ +runtime (depends on types + utils) + ↑ +plugins (depend on any combination) +``` + +Plugins should import types from `@quartz-community/types`, utility functions from `@quartz-community/utils`, and browser utilities from `@quartz-community/runtime`. This layering ensures plugins don't depend on Quartz core. + +## Plugin System + +Page types define how a category of pages is rendered. They are configured in the `pageTypes` array in `quartz.config.yaml`. + +Quartz v5 introduces a community plugin system. Plugins are standalone Git repositories that are cloned into `.quartz/plugins/` and re-exported through an auto-generated index file at `.quartz/plugins/index.ts`. + +### Plugin Types + +There are now four plugin categories: + +- **Transformers**: Map over content (parse frontmatter, generate descriptions, syntax highlighting) +- **Filters**: Filter content (remove drafts, explicit publish) +- **Emitters**: Reduce over content (generate RSS, sitemaps, alias redirects, OG images) +- **Page Types**: Define how pages are rendered. Each page type handles a specific kind of page (content notes, folder listings, tag listings, 404). The `PageTypeDispatcher` emitter routes pages to the appropriate page type plugin based on the content. +- **Bases Views**: Custom view renderers for the `bases-page` plugin's database-like view system. Plugins can register new view types (e.g., timeline, kanban) via the `ViewRegistry`. See [[making plugins#Bases Views]] for details. + +Note that plugin types are **not mutually exclusive** — a single plugin can be a transformer AND provide components (e.g., `obsidian-flavored-markdown`), or be a page type AND provide custom frames (e.g., `canvas-page`). + +### Plugin Resolution + +When `npx quartz plugin add github:quartz-community/explorer` is run: + +1. The repository is cloned into `.quartz/plugins/explorer/` +2. The plugin is built using `tsup` (defined in each plugin's `tsup.config.ts`) +3. An auto-generated `.quartz/plugins/index.ts` re-exports all installed plugins +4. The plugin's commit hash is recorded in `quartz.lock.json` + +### Plugin CLI Commands + +- `npx quartz plugin add github:quartz-community/` — Install a community plugin +- `npx quartz plugin install --latest` — Update all plugins to latest commits +- `npx quartz plugin install --clean` — Restore plugins from locked commits in `quartz.lock.json` (used in CI/CD) +- `npx quartz plugin remove ` — Remove an installed plugin + +### Plugin Structure + +Each community plugin repository contains: + +- `src/index.ts` — Plugin entry point exporting the plugin function +- `tsup.config.ts` — Build configuration using tsup +- `package.json` — Declares dependencies on `@quartz-community/types` and `@quartz-community/utils` + +The architecture and design of the plugin system was intentionally left pretty vague here as this is described in much more depth in the guide on [[making plugins|creating plugins]]. + +## Page Frames + +Page frames control the inner HTML structure of each page. While the outer shell (``, ``, ``, `#quartz-root`) is always the same (required for [[SPA Routing]]), the frame determines how layout slots are arranged inside the page. + +The frame system lives in `quartz/components/frames/` and consists of: + +- `types.ts` — Defines the `PageFrame` and `PageFrameProps` interfaces +- `DefaultFrame.tsx` — Three-column layout (left sidebar, center, right sidebar, footer) +- `FullWidthFrame.tsx` — No sidebars, single center column +- `MinimalFrame.tsx` — No sidebars, no header/beforeBody, just content and footer +- `registry.ts` — `FrameRegistry` singleton for plugin-registered frames +- `index.ts` — `resolveFrame()` function and built-in frame registry + +### Frame Registry + +The `FrameRegistry` (`quartz/components/frames/registry.ts`) is a singleton that stores frames registered by community plugins. It mirrors the design of the `ComponentRegistry`. Plugins declare frames in their `package.json` manifest under the `"quartz"."frames"` field, and these are loaded by `quartz/plugins/loader/frameLoader.ts` during plugin initialization. + +### Frame Resolution + +The rendering pipeline in `quartz/components/renderPage.tsx` delegates to the resolved frame's `render()` function. Frame resolution happens in the `PageTypeDispatcher` emitter (`quartz/plugins/pageTypes/dispatcher.ts`) using this priority: + +1. YAML config: `layout.byPageType..template` +2. Plugin-registered frame: looked up by name in the `FrameRegistry` +3. Built-in frame: looked up by name in the `builtinFrames` map +4. Fallback: `"default"` + +The active frame name is set as a `data-frame` attribute on the `.page` element, enabling frame-specific CSS overrides in `quartz/styles/base.scss`. + +### Plugin-Provided Frames + +Community plugins can ship their own frames by exporting them from a `./frames` subpath and declaring them in the plugin manifest. For example, the `canvas-page` plugin provides a `"canvas"` frame with a fullscreen layout and togglable sidebar. See [[making plugins#Providing Custom Frames]] for implementation details. + +See [[layout#Page Frames]] for user-facing documentation and [[making plugins#Page Types]] for how to set frames in page type plugins. diff --git a/Local/storage/thlab-notes/worker/docs/advanced/creating components.md b/Local/storage/thlab-notes/worker/docs/advanced/creating components.md new file mode 100644 index 0000000..bdffa40 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/advanced/creating components.md @@ -0,0 +1,266 @@ +--- +title: Creating Component Plugins +--- + +> [!warning] +> This guide assumes you have experience writing JavaScript and are familiar with TypeScript. + +Normally on the web, we write layout code using HTML which looks something like the following: + +```html +
+

An article header

+

Some content

+
+``` + +This piece of HTML represents an article with a leading header that says "An article header" and a paragraph that contains the text "Some content". This is combined with CSS to style the page and JavaScript to add interactivity. + +However, HTML doesn't let you create reusable templates. If you wanted to create a new page, you would need to copy and paste the above snippet and edit the header and content yourself. This isn't great if we have a lot of content on our site that shares a lot of similar layout. The smart people who created React also had similar complaints and invented the concept of Components -- JavaScript functions that return JSX -- to solve the code duplication problem. + +In effect, components allow you to write a JavaScript function that takes some data and produces HTML as an output. **While Quartz doesn't use React, it uses the same component concept to allow you to easily express layout templates in your Quartz site.** + +## Community Component Plugins + +In v5, most components are community plugins — standalone repositories that export a `QuartzComponent`. These plugins are decoupled from the core Quartz repository, allowing for easier maintenance and sharing. + +### Getting Started + +To create a new component plugin, you can use the official plugin template: + +```shell +git clone https://github.com/quartz-community/plugin-template.git my-component +cd my-component +npm install +``` + +### Plugin Structure + +A component plugin's `src/index.ts` typically exports a function (a constructor) that returns a `QuartzComponent`. This allows users to pass configuration options to your component. + +```tsx title="src/index.ts" +import { + QuartzComponent, + QuartzComponentConstructor, + QuartzComponentProps, +} from "@quartz-community/types" + +interface Options { + favouriteNumber: number +} + +const defaultOptions: Options = { + favouriteNumber: 42, +} + +const MyComponent: QuartzComponentConstructor = (userOpts?: Options) => { + const opts = { ...defaultOptions, ...userOpts } + + const Component: QuartzComponent = (props: QuartzComponentProps) => { + if (opts.favouriteNumber < 0) return null + return

My favourite number is {opts.favouriteNumber}

+ } + + return Component +} + +export default MyComponent +``` + +### Props + +All Quartz components accept the same set of props: + +```tsx +export type QuartzComponentProps = { + fileData: QuartzPluginData + cfg: GlobalConfiguration + tree: Node + allFiles: QuartzPluginData[] + displayClass?: "mobile-only" | "desktop-only" +} +``` + +- `fileData`: Any metadata plugins may have added to the current page. + - `fileData.slug`: slug of the current page. + - `fileData.frontmatter`: any frontmatter parsed. +- `cfg`: The `configuration` field in `quartz.config.yaml`. +- `tree`: the resulting [HTML AST](https://github.com/syntax-tree/hast) after processing and transforming the file. +- `allFiles`: Metadata for all files that have been parsed. Useful for doing page listings or figuring out the overall site structure. +- `displayClass`: a utility class that indicates a preference from the user about how to render it in a mobile or desktop setting. + +### Styling + +In community plugins, styles are bundled with the plugin. You can define styles using the `.css` property on the component: + +```tsx +Component.css = ` + .my-component { color: red; } +` +``` + +For SCSS, you can import it and assign it to the `.css` property. The build system will handle the transformation: + +```tsx +import styles from "./styles.scss" +Component.css = styles +``` + +> [!warning] +> Quartz does not use CSS modules so any styles you declare here apply _globally_. If you only want it to apply to your component, make sure you use specific class names and selectors. + +### Internationalization + +Component plugins should use the i18n pattern for any user-facing strings. See [[making plugins#Internationalization (i18n)]] for the full setup guide. + +Quick reference: + +```tsx +import { i18n } from "../i18n" + +const MyComponent: QuartzComponent = ({ cfg }) => { + const t = i18n(cfg.locale ?? "en-US").components.myComponent + return

{t.title}

+} +``` + +Always provide at least an `en-US` locale as the fallback. Additional locales are optional but encouraged for international reach. + +### Scripts and Interactivity + +For interactivity, you can declare `.beforeDOMLoaded` and `.afterDOMLoaded` properties on the component. These should be strings containing the JavaScript to be executed in the browser. + +- `.beforeDOMLoaded`: Executed _before_ the page is done loading. Used for prefetching or early initialization. +- `.afterDOMLoaded`: Executed once the page has been completely loaded. + +If you need to create an `afterDOMLoaded` script that depends on page-specific elements that may change when navigating, listen for the `"nav"` event: + +```ts +document.addEventListener("nav", () => { + // do page specific logic here + const toggleSwitch = document.querySelector("#switch") as HTMLInputElement + if (toggleSwitch) { + toggleSwitch.addEventListener("change", switchTheme) + window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) + } +}) +``` + +You can also use the `"prenav"` event, which fires before the page is replaced during SPA navigation. + +The `"render"` event fires when the DOM has been updated in-place without a full navigation — for example, after content decryption or dynamic DOM modifications by other plugins. If your component attaches event listeners to content elements, listen for `"render"` in addition to `"nav"` to ensure re-initialization: + +```ts +function setupMyComponent() { + const elements = document.querySelectorAll(".my-interactive") + for (const el of elements) { + el.addEventListener("click", handleClick) + window.addCleanup(() => el.removeEventListener("click", handleClick)) + } +} + +document.addEventListener("nav", setupMyComponent) +document.addEventListener("render", setupMyComponent) +``` + +It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks during SPA navigation. + +#### Importing Code + +In community plugins, TypeScript scripts should be transpiled at build time. The plugin template includes an `inlineScriptPlugin` in `tsup.config.ts` that automatically transpiles `.inline.ts` files imported as text: + +```tsx title="src/index.ts" +import script from "./script.inline.ts" + +const Component: QuartzComponent = (props) => { + return +} +Component.afterDOMLoaded = script +``` + +The `inlineScriptPlugin` handles transpiling TypeScript to browser-compatible JavaScript during the build step, allowing you to write type-safe client-side code. + +### Installing Your Component + +Once your component is published (e.g., to GitHub or npm), users can install it using the Quartz CLI: + +```shell +npx quartz plugin add github:your-username/my-component +``` + +Then, they can add it to their `quartz.config.yaml`: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:your-username/my-component + enabled: true + options: + favouriteNumber: 42 + layout: + position: left + priority: 60 +``` + +For advanced usage via the TS override in `quartz.ts`: + +```ts title="quartz.ts (override)" +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" +import Plugin from "./.quartz/plugins" + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout({ + byPageType: { + content: { + left: [Plugin.MyComponent({ favouriteNumber: 42 })], + }, + }, +}) +``` + +### Receiving YAML Options in Component-Only Plugins + +Component plugins that also belong to a processing category (transformer, filter, emitter, page type) receive options through their factory function automatically. However, **component-only plugins** — those whose manifest declares only `"category": ["component"]` — are loaded via side-effect import and don't go through the factory path. + +To receive YAML options in a component-only plugin, export an `init` function from your entry point: + +```ts title="src/index.ts" +export function init(options?: Record): void { + // options contains merged defaultOptions + user's YAML options + const myFlag = (options?.myFlag as boolean) ?? false + // Use options to configure registrations, global state, etc. +} +``` + +Quartz's config-loader calls `init()` after importing the module, passing the merged result of your manifest's `defaultOptions` and the user's `options` from `quartz.config.yaml`. The merge follows the same `{ ...defaultOptions, ...userOptions }` pattern used for processing plugins — user values take precedence. + +Declare your defaults in `package.json`: + +```json title="package.json" +{ + "quartz": { + "category": ["component"], + "defaultOptions": { + "myFlag": false + } + } +} +``` + +If your plugin does not export `init`, it continues to work as a pure side-effect import — this is fully backward compatible. + +## Internal Components + +Quartz also has internal components that provide layout utilities. These live in `quartz/components/` and are primarily used for structural purposes: + +- `Component.Head()` — renders the `` tag +- `Component.Spacer()` — adds flexible space +- `Component.Flex()` — flexible layout container +- `Component.MobileOnly()` — shows component only on mobile +- `Component.DesktopOnly()` — shows component only on desktop +- `Component.ConditionalRender()` — conditionally renders based on page data + +See [[layout-components]] for more details on these utilities. + +> [!hint] +> Look at existing community plugins like [Explorer](https://github.com/quartz-community/explorer) or [Darkmode](https://github.com/quartz-community/darkmode) for real-world examples. diff --git a/Local/storage/thlab-notes/worker/docs/advanced/index.md b/Local/storage/thlab-notes/worker/docs/advanced/index.md new file mode 100644 index 0000000..3d0da80 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/advanced/index.md @@ -0,0 +1,10 @@ +--- +title: "Advanced" +--- + +This section covers advanced topics for users who want to extend or deeply customize Quartz. + +- **[[architecture]]** — How Quartz works under the hood: the parse, filter, and emit pipeline +- **[[making plugins]]** — Build your own transformer, filter, emitter, or component plugin +- **[[creating components]]** — Create custom layout components with JSX +- **[[paths]]** — How Quartz resolves and transforms file paths diff --git a/Local/storage/thlab-notes/worker/docs/advanced/making plugins.md b/Local/storage/thlab-notes/worker/docs/advanced/making plugins.md new file mode 100644 index 0000000..9f53a59 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/advanced/making plugins.md @@ -0,0 +1,748 @@ +--- +title: Making your own plugins +--- + +> [!warning] +> This part of the documentation will assume you have working knowledge in TypeScript and will include code snippets that describe the interface of what Quartz plugins should look like. + +Quartz's plugins are a series of transformations over content. This is illustrated in the diagram of the processing pipeline below: + +![[quartz transform pipeline.png]] + +All plugins are defined as a function that takes in a single parameter for options `type OptionType = object | undefined` and return an object that corresponds to the type of plugin it is. + +```ts +type OptionType = object | undefined +type QuartzPlugin = (opts?: Options) => QuartzPluginInstance +type QuartzPluginInstance = + | QuartzTransformerPluginInstance + | QuartzFilterPluginInstance + | QuartzEmitterPluginInstance + | QuartzPageTypePluginInstance +``` + +The following sections will go into detail for what methods can be implemented for each plugin type. Before we do that, let's clarify a few more ambiguous types: + +- `BuildCtx` is defined in `@quartz-community/types`. It consists of + - `argv`: The command line arguments passed to the Quartz [[build]] command + - `cfg`: The full Quartz [[configuration]] + - `allSlugs`: a list of all the valid content slugs (see [[paths]] for more information on what a slug is) +- `StaticResources` is defined in `@quartz-community/types`. It consists of + - `css`: a list of CSS style definitions that should be loaded. A CSS style is described with the `CSSResource` type. It accepts either a source URL or the inline content of the stylesheet. + - `js`: a list of scripts that should be loaded. A script is described with the `JSResource` type. It allows you to define a load time (either before or after the DOM has been loaded), whether it should be a module, and either the source URL or the inline content of the script. + - `additionalHead`: a list of JSX elements or functions that return JSX elements to be added to the `` tag of the page. Functions receive the page's data as an argument and can conditionally render elements. + +## Getting Started + +In v5, plugins are standalone repositories. The easiest way to create one is using the plugin template: + +```shell +# Use the plugin template to create a new repository on GitHub +# Then clone it locally +git clone https://github.com/your-username/my-plugin.git +cd my-plugin +npm install +``` + +The template provides the build configuration (`tsup.config.ts`), TypeScript setup, and correct package structure. + +## Plugin Structure + +The basic file structure of a plugin is as follows: + +``` +my-plugin/ +├── src/ +│ └── index.ts # Plugin entry point +├── tsup.config.ts # Build configuration +├── package.json # Dependencies and exports +└── tsconfig.json # TypeScript configuration +``` + +The plugin's `package.json` should declare dependencies on `@quartz-community/types` (for type definitions) and optionally `@quartz-community/utils` (for shared utilities). + +## Plugin Types + +## Choosing a Plugin Type + +Quartz supports six plugin capabilities. A single plugin can combine multiple types. + +| I want to... | Plugin Type | +| ------------------------------------------------ | ----------- | +| Transform Markdown/HTML content | Transformer | +| Decide which pages to publish | Filter | +| Generate output files (RSS, sitemaps, manifests) | Emitter | +| Define how a category of pages renders | Page Type | +| Add a UI component to the layout | Component | +| Add a custom view to the Bases database system | Bases View | + +These are **not mutually exclusive**. For example: + +- `obsidian-flavored-markdown` is both a **transformer** (processes OFM syntax) and provides **components** (mermaid rendering) +- `canvas-page` is a **page type** that also provides a custom **frame** +- A plugin could be a **transformer** that adds metadata AND a **component** that displays it + +### Transformers + +Transformers **map** over content, taking a Markdown file and outputting modified content or adding metadata to the file itself. + +```ts +export type QuartzTransformerPluginInstance = { + name: string + textTransform?: (ctx: BuildCtx, src: string) => string + markdownPlugins?: (ctx: BuildCtx) => PluggableList + htmlPlugins?: (ctx: BuildCtx) => PluggableList + externalResources?: (ctx: BuildCtx) => Partial +} +``` + +All transformer plugins must define at least a `name` field to register the plugin and a few optional functions that allow you to hook into various parts of transforming a single Markdown file. + +- `textTransform` performs a text-to-text transformation _before_ a file is parsed into the [Markdown AST](https://github.com/syntax-tree/mdast). +- `markdownPlugins` defines a list of [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md). `remark` is a tool that transforms Markdown to Markdown in a structured way. +- `htmlPlugins` defines a list of [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md). Similar to how `remark` works, `rehype` is a tool that transforms HTML to HTML in a structured way. +- `externalResources` defines any external resources the plugin may need to load on the client-side for it to work properly. + +Normally for both `remark` and `rehype`, you can find existing plugins that you can use. If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library). + +A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[plugins/Latex|Latex]] plugin: + +```ts +import remarkMath from "remark-math" +import rehypeKatex from "rehype-katex" +import rehypeMathjax from "rehype-mathjax/svg" +import { QuartzTransformerPlugin } from "@quartz-community/types" + +interface Options { + renderEngine: "katex" | "mathjax" +} + +export const Latex: QuartzTransformerPlugin = (opts?: Options) => { + const engine = opts?.renderEngine ?? "katex" + return { + name: "Latex", + markdownPlugins() { + return [remarkMath] + }, + htmlPlugins() { + if (engine === "katex") { + // if you need to pass options into a plugin, you + // can use a tuple of [plugin, options] + return [[rehypeKatex, { output: "html" }]] + } else { + return [rehypeMathjax] + } + }, + externalResources() { + if (engine === "katex") { + return { + css: [ + { + // base css + content: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css", + }, + ], + js: [ + { + // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md + src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js", + loadTime: "afterDOMReady", + contentType: "external", + }, + ], + } + } + }, + } +} +``` + +Another common thing that transformer plugins will do is parse a file and add extra data for that file: + +```ts +import { QuartzTransformerPlugin } from "@quartz-community/types" + +export const AddWordCount: QuartzTransformerPlugin = () => { + return { + name: "AddWordCount", + markdownPlugins() { + return [ + () => { + return (tree, file) => { + // tree is an `mdast` root element + // file is a `vfile` + const text = file.value + const words = text.split(" ").length + file.data.wordcount = words + } + }, + ] + }, + } +} + +// tell typescript about our custom data fields we are adding +// other plugins will then also be aware of this data field +declare module "vfile" { + interface DataMap { + wordcount: number + } +} +``` + +Finally, you can also perform transformations over Markdown or HTML ASTs using the `visit` function from the `unist-util-visit` package or the `findAndReplace` function from the `mdast-util-find-and-replace` package. + +```ts +import { visit } from "unist-util-visit" +import { findAndReplace } from "mdast-util-find-and-replace" +import { QuartzTransformerPlugin } from "@quartz-community/types" +import { Link } from "mdast" + +export const TextTransforms: QuartzTransformerPlugin = () => { + return { + name: "TextTransforms", + markdownPlugins() { + return [ + () => { + return (tree, file) => { + // replace _text_ with the italics version + findAndReplace(tree, /_(.+)_/, (_value: string, ...capture: string[]) => { + // inner is the text inside of the () of the regex + const [inner] = capture + // return an mdast node + // https://github.com/syntax-tree/mdast + return { + type: "emphasis", + children: [{ type: "text", value: inner }], + } + }) + + // remove all links (replace with just the link content) + // match by 'type' field on an mdast node + // https://github.com/syntax-tree/mdast#link in this example + visit(tree, "link", (link: Link) => { + return { + type: "paragraph", + children: [{ type: "text", value: link.title }], + } + }) + } + }, + ] + }, + } +} +``` + +A parting word: transformer plugins are quite complex so don't worry if you don't get them right away. Take a look at the built in transformers and see how they operate over content to get a better sense for how to accomplish what you are trying to do. + +### Filters + +Filters **filter** content, taking the output of all the transformers and determining what files to actually keep and what to discard. + +```ts +export type QuartzFilterPlugin = ( + opts?: Options, +) => QuartzFilterPluginInstance + +export type QuartzFilterPluginInstance = { + name: string + shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean +} +``` + +A filter plugin must define a `name` field and a `shouldPublish` function that takes in a piece of content that has been processed by all the transformers and returns a `true` or `false` depending on whether it should be passed to the emitter plugins or not. + +For example, here is the built-in plugin for removing drafts: + +```ts +import { QuartzFilterPlugin } from "@quartz-community/types" + +export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ + name: "RemoveDrafts", + shouldPublish(_ctx, [_tree, vfile]) { + // uses frontmatter parsed from transformers + const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false + return !draftFlag + }, +}) +``` + +### Emitters + +Emitters **reduce** over content, taking in a list of all the transformed and filtered content and creating output files. + +```ts +export type QuartzEmitterPlugin = ( + opts?: Options, +) => QuartzEmitterPluginInstance + +export type QuartzEmitterPluginInstance = { + name: string + emit( + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + ): Promise | AsyncGenerator + partialEmit?( + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + changeEvents: ChangeEvent[], + ): Promise | AsyncGenerator | null + getQuartzComponents(ctx: BuildCtx): QuartzComponent[] +} +``` + +An emitter plugin must define a `name` field, an `emit` function, and a `getQuartzComponents` function. It can optionally implement a `partialEmit` function for incremental builds. + +- `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created. +- `partialEmit` is an optional function that enables incremental builds. It receives information about which files have changed (`changeEvents`) and can selectively rebuild only the necessary files. This is useful for optimizing build times in development mode. If `partialEmit` is undefined, it will default to the `emit` function. +- `getQuartzComponents` declares which Quartz components the emitter uses to construct its pages. + +Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `write` function in `@quartz-community/utils` if you are creating files that contain text. `write` has the following signature: + +```ts +export type WriteOptions = (data: { + // the build context + ctx: BuildCtx + // the name of the file to emit (not including the file extension) + slug: FullSlug + // the file extension + ext: `.${string}` | "" + // the file content to add + content: string +}) => Promise +``` + +This is a thin wrapper around writing to the appropriate output folder and ensuring that intermediate directories exist. If you choose to use the native Node `fs` APIs, ensure you emit to the `argv.output` folder as well. + +If you are creating an emitter plugin that needs to render components, there are three more things to be aware of: + +- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information. +- You can use the `renderPage` function defined in `@quartz-community/utils` to render Quartz components into HTML. +- If you need to render an HTML AST to JSX, you can use the `htmlToJsx` function from `@quartz-community/utils`. + +For example, the following is a simplified version of the content page plugin that renders every single page. + +```tsx +import { QuartzEmitterPlugin, FullPageLayout, QuartzComponentProps } from "@quartz-community/types" +import { renderPage, canonicalizeServer, pageResources, write } from "@quartz-community/utils" + +export const ContentPage: QuartzEmitterPlugin = () => { + return { + name: "ContentPage", + getQuartzComponents(ctx) { + const { head, header, beforeBody, pageBody, afterBody, left, right, footer } = ctx.cfg.layout + return [head, ...header, ...beforeBody, pageBody, ...afterBody, ...left, ...right, footer] + }, + async emit(ctx, content, resources): Promise { + const cfg = ctx.cfg.configuration + const fps: FilePath[] = [] + const allFiles = content.map((c) => c[1].data) + for (const [tree, file] of content) { + const slug = canonicalizeServer(file.data.slug!) + const externalResources = pageResources(slug, file.data, resources) + const componentData: QuartzComponentProps = { + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles, + } + + const content = renderPage(cfg, slug, componentData, {}, externalResources) + const fp = await write({ + ctx, + content, + slug: file.data.slug!, + ext: ".html", + }) + + fps.push(fp) + } + return fps + }, + } +} +``` + +Page types define how a category of pages is rendered. They are the primary way to add support for new file types or virtual pages in Quartz. + +```ts +export interface QuartzPageTypePluginInstance { + name: string + priority?: number + fileExtensions?: string[] + match: PageMatcher + generate?: PageGenerator + layout: string + frame?: string + body: QuartzComponentConstructor +} +``` + +- `name`: A unique identifier for this page type. +- `priority`: Controls matching order when multiple page types could match a slug. Higher priority page types are checked first. Default: `0`. +- `fileExtensions`: Array of file extensions this page type handles (e.g. `[".canvas"]`, `[".base"]`). Content files (`.md`) are handled by the default content page type. +- `match`: A function that determines whether a given slug/file should be rendered by this page type. +- `generate`: An optional function that produces virtual pages (pages not backed by files on disk, such as folder listings or tag indices). +- `layout`: The layout configuration key (e.g. `"content"`, `"folder"`, `"tag"`). This determines which `byPageType` entry in `quartz.config.yaml` provides the layout overrides for this page type. +- `frame`: The [[layout#Page Frames|page frame]] to use for this page type. Controls the overall HTML structure (e.g. `"default"`, `"full-width"`, `"minimal"`, or a custom frame provided by your plugin). If not set, defaults to `"default"`. Can be overridden per-page-type via `layout.byPageType..template` in `quartz.config.yaml`. +- `body`: The Quartz component constructor that renders the page body content. + +### Providing Custom Frames + +Plugins can ship their own [[layout#Page Frames|page frames]] — custom page layouts that control how the HTML structure (sidebars, header, content area, footer) is arranged. This is useful for page types that need fundamentally different layouts (e.g. a fullscreen canvas, a presentation mode, a dashboard). + +To provide a custom frame: + +**1. Create the frame file:** + +```tsx title="src/frames/MyFrame.tsx" +import type { PageFrame, PageFrameProps } from "@quartz-community/types" +import type { ComponentChildren } from "preact" + +export const MyFrame: PageFrame = { + name: "my-frame", + css: ` +.page[data-frame="my-frame"] > #quartz-body { + grid-template-columns: 1fr; + grid-template-areas: "center"; +} +`, + render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps): unknown { + const renderSlot = (C: (props: typeof componentData) => unknown): ComponentChildren => + C(componentData) as ComponentChildren + return ( +
+ {(Content as any)(componentData)} + {(Footer as any)(componentData)} +
+ ) + }, +} +``` + +Key requirements: + +- `name`: A unique string identifier. This is what page types and YAML config reference. +- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure. +- `css` (optional): Frame-specific CSS. Scope it with `.page[data-frame="my-frame"]` selectors to avoid conflicts. + +**2. Re-export the frame:** + +```ts title="src/frames/index.ts" +export { MyFrame } from "./MyFrame" +``` + +**3. Declare the frame in `package.json`:** + +```json title="package.json" +{ + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./frames": { + "import": "./dist/frames/index.js", + "types": "./dist/frames/index.d.ts" + } + }, + "quartz": { + "frames": { + "MyFrame": { "exportName": "MyFrame" } + } + } +} +``` + +The `"frames"` field in the `"quartz"` manifest maps export names to frame metadata. The key (e.g. `"MyFrame"`) must match the export name in `src/frames/index.ts`. + +**4. Add the frame entry point to your build config:** + +```ts title="tsup.config.ts" +export default defineConfig({ + entry: ["src/index.ts", "src/frames/index.ts"], + // ... +}) +``` + +**5. Reference the frame in your page type:** + +```ts +export const MyPageType: QuartzPageTypePlugin = () => ({ + name: "MyPageType", + frame: "my-frame", // References the frame by its name property + // ... +}) +``` + +When a user installs your plugin, Quartz automatically loads the frame from the `./frames` export and registers it in the Frame Registry. The frame is then available by name in any page type or YAML config override. + +> [!tip] +> See the [`canvas-page`](https://github.com/quartz-community/canvas-page) plugin for a complete real-world example of a plugin-provided frame. + +### Bases Views + +The `bases-page` plugin provides a database-like view system similar to Obsidian Bases. Other plugins can register custom view types via the `ViewRegistry`: + +```ts +import { viewRegistry } from "@quartz-community/bases-page"; +import type { ViewTypeRegistration } from "@quartz-community/bases-page"; + +viewRegistry.register({ + id: "timeline", + name: "Timeline", + icon: "git-branch", + render: ({ entries, view, slug, allSlugs }) => ( +
+ {entries.map(entry =>
{entry.properties.title}
)} +
+ ), + css: `.bases-timeline { display: flex; flex-direction: column; }`, + afterDOMLoaded: `document.addEventListener("nav", () => { /* setup */ })`, +}); +``` + +Each view registration includes: + +- `id`: Unique identifier (e.g., `"timeline"`, `"kanban"`) +- `name`: Display name shown in the view selector +- `icon`: Optional Lucide icon name +- `render`: Function that receives `ViewRendererProps` and returns Preact JSX +- `css`: Optional CSS string (deduplicated by view ID) +- `afterDOMLoaded`: Optional client-side script (same lifecycle as component scripts) +- `options`: Optional configuration passed to every render invocation + +The `ViewRegistry` is a global singleton (via `Symbol.for`) ensuring all copies of the module share the same registry. + +## Building and Distribution + +Quartz v5 plugins ship pre-built `dist/` in their repositories. When a user installs your plugin, Quartz detects the pre-built output and skips the install/build cycle entirely — making installation near-instant. + +### Build Configuration + +The plugin template's `tsup.config.ts` bundles all dependencies by default. Only **singleton externals** — packages that must be the same instance across all plugins — are left unbundled: + +```ts +const SINGLETON_EXTERNALS = [ + "preact", + "preact/hooks", + "preact/jsx-runtime", + "preact/compat", + "@jackyzha0/quartz", + "@jackyzha0/quartz/*", + "vfile", + "vfile/*", + "unified", +] + +export default defineConfig({ + // ... + noExternal: [/.*/], // Bundle everything + external: SINGLETON_EXTERNALS, // Except singletons +}) +``` + +This means your plugin's `dist/index.js` is self-contained — no `npm install` needed at install time. + +### Shipping Pre-built Output + +Your plugin's `dist/` directory should be committed to the repository: + +1. **Do NOT add `dist/` to `.gitignore`** +2. Run `npm run build` before committing +3. The CI workflow verifies `dist/` is up to date on every push + +If `dist/` is missing or gitignored, Quartz falls back to the full install/build cycle (useful during local development with symlinked plugins). + +### Plugins with Native Dependencies + +Plugins that require native packages (e.g. `sharp` for image processing) cannot bundle those. For these plugins: + +1. Set `"requiresInstall": true` in your `package.json` quartz manifest +2. Declare the native package as a `peerDependency` +3. Quartz will install it into the host project at build time + +```shell +# Build the plugin +npm run build +# or +npx tsup +``` + +## What to Import from Where + +| You need... | Import from | +| ---------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Type definitions (`QuartzTransformerPlugin`, `QuartzComponent`, etc.) | `@quartz-community/types` | +| Path utilities (`simplifySlug`, `resolveRelative`, `pathToRoot`) | `@quartz-community/utils/path` | +| DOM utilities (`removeAllChildren`, `registerEscapeHandler`) | `@quartz-community/utils/dom` | +| JSX conversion (`htmlToJsx`) | `@quartz-community/utils/jsx` | +| Language utilities (`classNames`, `capitalize`) | `@quartz-community/utils/lang` | +| Date/sort utilities (`formatDate`, `getDate`, `byDateAndAlphabetical`) | `@quartz-community/utils/date` and `@quartz-community/utils/sort` | +| HTML escaping (`escapeHTML`, `unescapeHTML`) | `@quartz-community/utils/escape` | +| Emoji utilities (`getIconCode`) | `@quartz-community/utils/emoji` | +| Browser runtime (`onNav`, `onRender`, `fetchContentIndex`) | `@quartz-community/runtime` | + +Do **not** import from `@jackyzha0/quartz` or from `vfile` directly. Use the community packages instead. + +## Internationalization (i18n) + +Plugins should provide their own translations for user-facing strings. Do **not** hardcode strings in components. + +### Setting Up i18n + +Create the following structure: + +``` +src/i18n/ +├── index.ts +└── locales/ + └── en-US.ts +``` + +**`src/i18n/locales/en-US.ts`** (required base locale): + +```ts +export default { + components: { + myPlugin: { + title: "My Plugin", + description: "A description", + itemCount: ({ count }: { count: number }) => (count === 1 ? "1 item" : `${count} items`), + }, + }, +} +``` + +**`src/i18n/index.ts`**: + +```ts +import enUS from "./locales/en-US" + +const locales: Record = { + "en-US": enUS, +} + +export function i18n(locale: string) { + return locales[locale] || enUS +} +``` + +### Using i18n in Components + +```tsx +import { i18n } from "../i18n" + +const MyComponent: QuartzComponent = ({ cfg }) => { + const locale = cfg.locale ?? "en-US" + const t = i18n(locale).components.myPlugin + return

{t.title}

+} +``` + +### Adding Translations + +To add a new locale, copy `en-US.ts`, translate the strings, and register it: + +```ts +// src/i18n/locales/fr-FR.ts +export default { + components: { + myPlugin: { + title: "Mon Plugin", + description: "Une description", + itemCount: ({ count }: { count: number }) => + count === 1 ? "1 élément" : `${count} éléments`, + }, + }, +} +``` + +```ts +// src/i18n/index.ts +import enUS from "./locales/en-US" +import frFR from "./locales/fr-FR" + +const locales: Record = { + "en-US": enUS, + "fr-FR": frFR, +} +``` + +Use [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) locale codes (e.g., `en-US`, `de-DE`, `ja-JP`, `zh-CN`). For dynamic content, use function-based translations as shown with `itemCount` above. + +## Installing Your Plugin + +```shell +# In your Quartz project +npx quartz plugin add github:your-username/my-plugin +``` + +This clones the plugin and adds it to both `quartz.config.yaml` and `quartz.lock.json`. If the plugin ships pre-built `dist/` (recommended), installation completes in seconds with no build step. You can then configure it in your config: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:your-username/my-plugin + enabled: true +``` + +For options that require JavaScript callback functions (not expressible in YAML), use the TS override in `quartz.ts`: + +```ts title="quartz.ts (override)" +import * as ExternalPlugin from "./.quartz/plugins" + +// Must be placed before loadQuartzConfig() +ExternalPlugin.MyPlugin({ + customFn: (data) => { + // ... + }, +}) +``` + +Options set via `quartz.ts` are merged with YAML options at instantiation time, with `quartz.ts` overrides taking precedence. These calls must be placed **before** `loadQuartzConfig()` in your `quartz.ts`. + +### Development Workflow + +During plugin development, you'll frequently install and uninstall your plugin to test changes. The following commands help manage this cycle: + +```shell +# Remove your plugin and clean up +npx quartz plugin remove my-plugin + +# Re-add after making changes +npx quartz plugin add github:your-username/my-plugin +``` + +If you've updated your `quartz.config.yaml` to reference a plugin that isn't installed yet, you can install it without manually running `add`: + +```shell +# Install all config-referenced plugins missing from the lockfile +npx quartz plugin install --from-config + +# Preview first without making changes +npx quartz plugin install --from-config --dry-run +``` + +To clean up plugins that are installed but no longer referenced in your config: + +```shell +# Remove orphaned plugins +npx quartz plugin prune + +# Preview first without making changes +npx quartz plugin prune --dry-run +``` + +> [!tip] +> Both `resolve` and `prune` fall back to `quartz.config.default.yaml` if no `quartz.config.yaml` is present. This is useful for CI environments where the default config is the source of truth. See [[cli/plugin#prune|prune]] and [[cli/plugin#resolve|resolve]] for full details. + +## Component Plugins + +For plugins that provide visual components (like Explorer, Graph, Search), see the [[creating components|creating component plugins]] guide. + +Component-only plugins (those with `"category": ["component"]` in their manifest) are loaded via side-effect import rather than a factory function. If your component-only plugin needs to receive user options from `quartz.config.yaml`, export an `init(options)` function — see [[creating components#Receiving YAML Options in Component-Only Plugins|receiving YAML options]] for details. diff --git a/Local/storage/thlab-notes/worker/docs/advanced/paths.md b/Local/storage/thlab-notes/worker/docs/advanced/paths.md new file mode 100644 index 0000000..16f6388 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/advanced/paths.md @@ -0,0 +1,51 @@ +--- +title: Paths in Quartz +--- + +Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places. + +A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path. + +It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it. + +Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.org/play#example/nominal-typing). + +```typescript +// instead of +type FullSlug = string + +// we do +type FullSlug = string & { __brand: "full" } + +// that way, the following will fail typechecking +const slug: FullSlug = "some random string" +``` + +While this prevents most typing mistakes _within_ our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from _accidentally_ mistaking a string for a client slug when we forcibly cast it. + +Thus, we still need to be careful when casting from a string to one of these nominal types in the 'entrypoints', illustrated with hexagon shapes in the diagram below. + +The following diagram draws the relationships between all the path sources, nominal path types, and what functions in `quartz/path.ts` convert between them. + +```mermaid +graph LR + Browser{{Browser}} --> Window{{Body}} & LinkElement{{Link Element}} + Window --"getFullSlug()"--> FullSlug[Full Slug] + LinkElement --".href"--> Relative[Relative URL] + FullSlug --"simplifySlug()" --> SimpleSlug[Simple Slug] + SimpleSlug --"pathToRoot()"--> Relative + SimpleSlug --"resolveRelative()" --> Relative + MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links] + Links --"transformLink()"--> Relative + FilePath --"slugifyFilePath()"--> FullSlug[Full Slug] + style FullSlug stroke-width:4px +``` + +Here are the main types of slugs with a rough description of each type of path: + +- `FilePath`: a real file path to a file on disk. Cannot be relative and must have a file extension. +- `FullSlug`: cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. +- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. +- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash. + +To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/util/path.test.ts`. diff --git a/Local/storage/thlab-notes/worker/docs/cli/build.md b/Local/storage/thlab-notes/worker/docs/cli/build.md new file mode 100644 index 0000000..7cbf7d4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/build.md @@ -0,0 +1,78 @@ +--- +title: quartz build +aliases: + - build +--- + +The `build` command transforms your Markdown content into a static HTML website. It processes your files through the configured plugins and outputs the final site to a directory of your choice. + +## Flags + +| Flag | Shorthand | Description | Default | +| ----------------- | --------- | --------------------------------------------------------- | ----------------- | +| `--directory` | `-d` | The directory containing your Quartz project | Current directory | +| `--verbose` | `-v` | Enable detailed logging for debugging | `false` | +| `--output` | `-o` | The directory where the built site will be saved | `public` | +| `--serve` | | Start a local development server | `false` | +| `--watch` | | Rebuild the site when files change | `false` | +| `--port` | | The port for the development server | `8080` | +| `--wsPort` | | The port for the WebSocket hot-reload server | `3001` | +| `--baseDir` | | Set a base directory for the site (e.g. for GitHub Pages) | `/` | +| `--remoteDevHost` | | The hostname to use for the development server | `localhost` | +| `--bundleInfo` | | Output a JSON file with bundle size information | `false` | +| `--concurrency` | `-c` | Number of worker threads to use for building | CPU core count | + +## Examples + +### Basic Build + +Generate your site into the `public` folder. + +```shell +npx quartz build +``` + +### Development Mode + +Start a local server and watch for changes. This is the most common way to preview your site while writing. + +```shell +npx quartz build --serve +``` + +### Custom Output and Port + +Build to a specific folder and run the server on a different port. + +```shell +npx quartz build --serve --output dist --port 3000 +``` + +### Performance Tuning + +If you have a very large vault, you can limit the number of concurrent workers to save memory. + +```shell +npx quartz build --concurrency 2 +``` + +## Serve vs Watch + +The `--serve` and `--watch` flags control different behaviors: + +- **`--serve`** starts a local development server AND automatically watches for changes (implies `--watch`). This is the recommended mode for local development. +- **`--watch`** only watches for file changes and rebuilds automatically, without starting a server. This is useful for CI pipelines or custom server setups where you want automatic rebuilds but handle serving separately. + +In most cases, you want `--serve`: + +```shell +npx quartz build --serve +``` + +## Development Server + +The `--serve` flag starts a local web server. This server is intended for development and previewing only. It is not designed for production use. For information on how to deploy your site, see [[hosting]]. + +### Hot Reloading + +When running with `--serve`, Quartz automatically enables `--watch`. It uses a WebSocket connection (on the port specified by `--wsPort`) to notify your browser when a file has changed. The browser will then automatically refresh to show the latest version of your content. diff --git a/Local/storage/thlab-notes/worker/docs/cli/create.md b/Local/storage/thlab-notes/worker/docs/cli/create.md new file mode 100644 index 0000000..227d852 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/create.md @@ -0,0 +1,83 @@ +--- +title: quartz create +--- + +The `create` command initializes a new Quartz project. It helps you set up your content folder, choose a configuration template, set your site's base URL, and configure how Quartz should handle your Markdown files. + +## Flags + +| Flag | Shorthand | Description | +| ------------- | --------- | --------------------------------------------------------------------- | +| `--template` | `-t` | Configuration template (`default`, `obsidian`, `ttrpg`, or `blog`) | +| `--directory` | `-d` | The directory where Quartz will be initialized | +| `--source` | `-s` | The source directory of your Markdown files | +| `--strategy` | `-X` | How to handle the source files (`new`, `copy`, or `symlink`) | +| `--links` | `-l` | How to resolve internal links (`absolute`, `shortest`, or `relative`) | +| `--baseUrl` | `-b` | Base URL for your site (e.g. `mysite.github.io/quartz`) | +| `--verbose` | `-v` | Enable detailed logging | + +## Templates + +When you run `quartz create`, you can choose a configuration template that pre-configures Quartz for your use case. The selected template always overwrites `quartz.config.yaml`, even if one already exists. After applying the template, Quartz automatically runs plugin resolution to install any plugins the template requires and remove any that are no longer referenced. + +- **Default**: A clean Quartz setup with sensible defaults. Best for starting from scratch. +- **Obsidian**: Optimized for Obsidian vaults with full Obsidian Flavored Markdown support (wikilinks, callouts, mermaid diagrams, etc.). Automatically sets link resolution to `shortest` and skips the link resolution prompt. +- **TTRPG**: Builds on the Obsidian template with the addition of the [Leaflet bases plugin](https://github.com/Requiae/quartz-leaflet-bases-plugin) and [ITS Theme](https://github.com/saberzero1/quartz-themes) (`its-theme.ttrpg-dnd`). Great for D&D and TTRPG wikis. Also skips the link resolution prompt. +- **Blog**: A blog-focused setup with [recent notes](https://github.com/quartz-community/recent-notes) enabled (showing the 5 most recent posts with tags) and [comments](https://github.com/quartz-community/comments) enabled via giscus. You'll need to fill in the `TODO:` placeholder values in `quartz.config.yaml` with your own giscus repository details. + +## Base URL + +During setup, Quartz will ask for the base URL of your site. This is the URL where your site will be deployed (e.g. `mysite.github.io/quartz`). + +- Do **not** include the protocol (`https://`) — if you do, it will be automatically stripped. +- Trailing slashes are also removed automatically. +- See [[configuration]] for more details on how `baseUrl` is used. + +## Strategies + +When you run `quartz create`, you must choose a strategy for your content: + +- **new**: Creates a fresh, empty content folder. Use this if you are starting a new project from scratch. +- **copy**: Copies all files from your source directory into the Quartz content folder. This is the safest option for existing vaults as it doesn't touch your original files. +- **symlink**: Creates a symbolic link from the Quartz content folder to your source directory. Any changes you make in your source directory (e.g. in Obsidian) will be immediately reflected in Quartz. + +## Link Resolution + +Quartz needs to know how to interpret the internal links in your Markdown files: + +- **shortest**: Resolves links to the closest matching file name. This is the default for Obsidian. +- **absolute**: Resolves links relative to the root of your content folder. +- **relative**: Resolves links relative to the current file's location. + +> [!note] +> When using the **Obsidian** or **TTRPG** templates, link resolution is automatically set to `shortest` and the prompt is skipped. + +## Interactive Walkthrough + +If you run `npx quartz create` without any arguments, it will guide you through an interactive setup: + +1. **Choose a template**: Select a configuration template (`Default`, `Obsidian`, `TTRPG`, or `Blog`). +2. **Select a strategy**: Choose between `new`, `copy`, or `symlink`. +3. **Enter base URL**: Provide the URL where your site will be hosted. +4. **Select link resolution**: Choose how your links are formatted (skipped for Obsidian and TTRPG templates). +5. **Finish**: Quartz will set up the directory structure, create your configuration, and automatically install any plugins referenced in the template. + +## Example: Importing an Obsidian Vault + +To create a Quartz project that links directly to an existing Obsidian vault: + +```shell +npx quartz create --template obsidian --strategy symlink --source ~/Documents/MyVault +``` + +This command tells Quartz to use the Obsidian template (with full OFM support and shortest link resolution), look at your vault in `~/Documents/MyVault`, and use symbolic links so changes are synced. + +## Example: Setting Up a Blog + +To quickly set up a blog with recent notes and comments: + +```shell +npx quartz create --template blog --strategy new --baseUrl myblog.github.io +``` + +After setup, edit `quartz.config.yaml` to fill in your giscus repository details in the comments plugin section. diff --git a/Local/storage/thlab-notes/worker/docs/cli/index.md b/Local/storage/thlab-notes/worker/docs/cli/index.md new file mode 100644 index 0000000..d02b915 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/index.md @@ -0,0 +1,55 @@ +--- +title: CLI Reference +--- + +The Quartz CLI is the primary way to interact with your Quartz project. It provides commands for creating new projects, building static sites, syncing with GitHub, and managing plugins. + +You can run the CLI using `npx quartz`. + +## Quick Reference + +| Command | Description | Example | +| --------- | ------------------------------------------------------- | ------------------------ | +| `create` | Initialize a new Quartz project with template selection | `npx quartz create` | +| `build` | Generate static HTML files | `npx quartz build` | +| `sync` | Sync content with GitHub | `npx quartz sync` | +| `upgrade` | Upgrade Quartz to the latest version (alias: `update`) | `npx quartz upgrade` | +| `plugin` | Manage Quartz plugins (install, add, remove, etc.) | `npx quartz plugin list` | +| `tui` | Launch the interactive plugin manager | `npx quartz tui` | + +## Commands + +- [[create|create]]: Initialize a new Quartz project with a choice of templates (default, obsidian, ttrpg, blog) and base URL configuration. +- [[build|build]]: Build your Quartz site into static HTML. Includes a development server. +- [[sync|sync]]: Push and pull changes between your local machine and GitHub. +- [[upgrade|upgrade]]: Upgrade the Quartz framework to the latest version. Also available as `npx quartz update`. +- [[restore|restore]]: Recover your content folder from the local cache. +- [[cli/plugin|plugin]]: Install, add, remove, prune, and configure plugins. Use `plugin install` with flags for lockfile/config sync, updates, and checks. +- [[tui|tui]]: Use a terminal interface to manage plugins and layout. + +## Global Flags + +These flags are accepted by every Quartz command: + +| Flag | Shorthand | Description | Default | +| --------------- | --------- | ------------------------------------------------------------------------------------------------------- | -------------- | +| `--directory` | `-d` | The directory containing your Quartz project | `content` | +| `--verbose` | `-v` | Enable detailed logging for debugging | `false` | +| `--concurrency` | `-c` | Max parallel workers for operations that run in parallel (e.g. `build`, `plugin install`, `plugin add`) | CPU core count | + +Commands that don't perform parallel work accept `-c` as a no-op, so it's always safe to pass. See [[build#Performance Tuning|build]] and [[cli/plugin#Installing on low-end hardware|plugin]] for practical examples. + +## Help and Versioning + +To see a full list of available flags for any command, use the `--help` flag. + +```shell +npx quartz --help +npx quartz build --help +``` + +To check which version of Quartz you are currently running, use the `--version` flag. + +```shell +npx quartz --version +``` diff --git a/Local/storage/thlab-notes/worker/docs/cli/plugin.md b/Local/storage/thlab-notes/worker/docs/cli/plugin.md new file mode 100644 index 0000000..ba42893 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/plugin.md @@ -0,0 +1,281 @@ +--- +title: quartz plugin +--- + +The `plugin` command is the heart of the Quartz v5 plugin management system. it allows you to install, configure, and update plugins directly from the command line. + +All plugins are stored in the `.quartz/plugins/` directory, and their versions are tracked in `quartz.lock.json`. + +## Subcommands + +### list + +List all currently installed plugins and their versions. + +```shell +npx quartz plugin list +``` + +### add + +Add a new plugin from a Git repository. + +```shell +npx quartz plugin add github:username/repo +``` + +To install from a specific branch or ref, append `#ref` to the source: + +```shell +npx quartz plugin add github:username/repo#my-branch +npx quartz plugin add git+https://github.com/username/repo.git#my-branch +npx quartz plugin add https://github.com/username/repo.git#my-branch +``` + +You can also add a plugin from a local directory. This is useful for local development or airgapped environments: + +```shell +npx quartz plugin add ./path/to/my-plugin +npx quartz plugin add ../sibling-plugin +npx quartz plugin add /absolute/path/to/plugin +``` + +Local plugins are symlinked into `.quartz/plugins/`, so any changes you make to the source directory are reflected immediately without re-installing. + +When a branch is specified, it is stored in the lockfile. All subsequent commands (`install`, `prune`) will respect that branch automatically. Use `install --latest` to fetch the latest commit from that branch. + +> [!tip] +> `plugin add` also accepts `--concurrency` / `-c` to limit how many remote repositories are cloned and built at the same time. This is the same flag documented under [[#install]] and is useful when adding several plugins at once on low-end hardware. + +### remove + +Remove an installed plugin. + +```shell +npx quartz plugin remove plugin-name +``` + +### install + +Install plugins for your Quartz project. By default, this installs all plugins listed in your `quartz.lock.json` file. + +```shell +npx quartz plugin install +``` + +#### Flags + +- `--from-config`: Synchronize plugins with `quartz.config.yaml` instead of the lockfile. This will install missing plugins and prune orphaned ones. +- `--latest`: Fetch the latest version of plugins from their remote sources instead of using the version in the lockfile. +- `--clean`: Skip existing directories and perform a fresh installation. +- `--dry-run`: Preview the changes without actually installing or removing any files. +- `--concurrency`, `-c`: Maximum number of plugins to clone, fetch, and build in parallel. Defaults to the number of CPU cores. Lower this (e.g. `-c 1` or `-c 2`) on memory- or CPU-constrained machines where the default parallelism causes failures, OOMs, or hangs. See [[#Installing on low-end hardware]] below. + +#### Positional Arguments + +- `[names..]`: Optional list of specific plugin names to install or update. + +```shell +# Update specific plugins to latest +npx quartz plugin install --latest plugin-a plugin-b + +# Preview what would be installed from config +npx quartz plugin install --from-config --dry-run +``` + +### enable / disable + +Toggle a plugin's status in your `quartz.config.yaml` without removing its files. + +```shell +npx quartz plugin enable plugin-name +npx quartz plugin disable plugin-name +``` + +### config + +View or modify the configuration for a specific plugin. + +```shell +# View config +npx quartz plugin config plugin-name + +# Set a value +npx quartz plugin config plugin-name --set key=value +``` + +### prune + +Remove installed plugins that are no longer referenced in your `quartz.config.yaml`. This is useful for cleaning up after removing plugin entries from your configuration. + +> [!note] +> Running `plugin install --from-config` also removes orphaned plugins as part of its synchronization. Use `prune` when you only want to clean up without installing anything new. + +```shell +npx quartz plugin prune +``` + +Use `--dry-run` to preview which plugins would be removed without making changes: + +```shell +npx quartz plugin prune --dry-run +``` + +## Common Workflows + +### Adding and Enabling a Plugin + +To add a new plugin and start using it: + +1. Add the plugin: `npx quartz plugin add github:quartz-community/example` +2. Enable it: `npx quartz plugin enable example` + +### Updating Everything + +To keep your plugins fresh: + +```shell +npx quartz plugin install --latest +``` + +### Installing on low-end hardware + +By default, `plugin install` and `plugin add` clone, fetch, and build plugins in parallel across all your CPU cores. On memory-constrained machines (low-end laptops, Raspberry Pi, small VPS instances, restrictive CI runners) this can exhaust RAM or overwhelm the system because each worker may kick off its own `npm install` / `npm run build` at the same time. + +> [!note] +> Most community plugins now ship with a pre-built `dist/` directory. When Quartz finds this, it skips the installation and build steps entirely, making the process much faster and lighter on resources. This section is primarily relevant for plugins in development or those that don't provide pre-built distribution. + +If `plugin install` fails, hangs, or OOMs on your machine, lower the concurrency with `--concurrency` / `-c`: + +```shell +# Install one plugin at a time (safest, slowest) +npx quartz plugin install --latest -c 1 + +# Two at a time — usually a good balance on 4 GB machines +npx quartz plugin install --latest --concurrency 2 +``` + +The same flag works on `plugin add` and the other plugin subcommands that perform parallel work: + +```shell +npx quartz plugin add github:quartz-community/some-plugin -c 1 +``` + +### Managing Configuration + +If you want to change a plugin setting without opening the YAML file: + +```shell +npx quartz plugin config explorer --set useSavedState=true +``` + +### Cleaning Up Unused Plugins + +If you've removed plugins from your config and want to clean up leftover files: + +```shell +npx quartz plugin prune --dry-run # preview first +npx quartz plugin prune # remove orphaned plugins +``` + +### Setting Up from Config + +When setting up on a new machine or in CI, `install --from-config` ensures your installed plugins match your config — installing missing plugins and removing any that are no longer referenced: + +```shell +npx quartz plugin install --from-config +``` + +### Testing with Branches + +If a plugin author has a fix or feature on a separate branch, you can install it directly without waiting for a release to the default branch: + +```shell +# Install from a feature branch +npx quartz plugin add github:username/repo#fix/some-bug + +# Later, switch back to the default branch by re-adding without a ref +npx quartz plugin remove repo +npx quartz plugin add github:username/repo +``` + +The branch ref is tracked in `quartz.lock.json`, so `install --latest` will continue to follow the specified branch until the plugin is re-added without one. + +Both `prune` and `install --from-config` will fall back to `quartz.config.default.yaml` if no `quartz.config.yaml` is present. + +### Local Plugin Development + +For local plugin development or airgapped environments, you can add a plugin from a local directory: + +```shell +npx quartz plugin add ./my-local-plugin +``` + +Local plugins are symlinked into `.quartz/plugins/`, so changes reflect immediately. When you run `install --latest`, local plugins are rebuilt (npm install + npm run build) without any git operations. + +> [!note] +> Local symlinked plugins typically use this build-on-install fallback because the `dist/` directory is usually gitignored during development. + +The `install --latest --dry-run` command will show local plugins with a "local" status instead of checking for remote updates. + +To switch a local plugin back to a git source: + +```shell +npx quartz plugin remove my-local-plugin +npx quartz plugin add github:username/my-local-plugin +``` + +### Subdirectory (Monorepo) Plugins + +Some plugins live in a subdirectory of a larger repository rather than at the root. For these, you can specify the plugin source as an object in `quartz.config.yaml` with a `subdir` field: + +```yaml title="quartz.config.yaml" +plugins: + - source: + repo: "https://github.com/username/monorepo.git" + subdir: plugin + enabled: true +``` + +This tells Quartz to clone the full repository but install only the contents of the specified subdirectory. + +You can combine `subdir` with `ref` to pin a branch or tag, and `name` to override the plugin directory name: + +```yaml title="quartz.config.yaml" +plugins: + - source: + repo: "https://github.com/username/monorepo.git" + subdir: packages/my-plugin + ref: v2.0 + name: my-plugin + enabled: true +``` + +See [[configuration#Advanced Source Options|Advanced Source Options]] for the full reference on object source fields. + +> [!note] +> The `plugin add` CLI command works with string sources. To use the object source format with `subdir`, edit `quartz.config.yaml` directly, then run `npx quartz plugin install --from-config` to install it. + +## Migration from Deprecated Commands + +| Old command | New equivalent | +| ------------------------------------- | --------------------------------------------------- | +| `npx quartz plugin restore` | `npx quartz plugin install --clean` | +| `npx quartz plugin update` | `npx quartz plugin install --latest` | +| `npx quartz plugin update my-plugin` | `npx quartz plugin install --latest my-plugin` | +| `npx quartz plugin check` | `npx quartz plugin install --latest --dry-run` | +| `npx quartz plugin resolve` | `npx quartz plugin install --from-config` | +| `npx quartz plugin resolve --dry-run` | `npx quartz plugin install --from-config --dry-run` | +| `npx quartz update` | `npx quartz plugin install --latest` | + +The old commands still work as hidden aliases but will print a deprecation warning. + +## Plugin Status + +Running the plugin command without any subcommand shows a status dashboard of all installed plugins, including whether updates are available: + +```shell +npx quartz plugin +``` + +This displays each plugin with its source, commit, enabled/disabled status, and checks for available updates in parallel. For the full interactive management interface, use [[tui|npx quartz tui]] instead. diff --git a/Local/storage/thlab-notes/worker/docs/cli/restore.md b/Local/storage/thlab-notes/worker/docs/cli/restore.md new file mode 100644 index 0000000..ca07d77 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/restore.md @@ -0,0 +1,31 @@ +--- +title: quartz restore +--- + +The `restore` command is a safety mechanism that allows you to recover your **content folder** from a local cache. This command only affects your Markdown files and does not restore plugins or configuration. + +To restore plugins to a specific state, use [[cli/plugin|npx quartz plugin install]]. + +## When to Use + +You should use `restore` if: + +- A `quartz upgrade` failed and corrupted your content. +- You accidentally deleted files in your content folder. +- You encountered complex merge conflicts that you want to undo. + +## How it Works + +Quartz maintains a hidden cache of your content folder. Every time you run certain commands, Quartz ensures that a backup of your Markdown files exists. The `restore` command simply copies these files back into your main content directory. + +```shell +npx quartz restore +``` + +## Example Workflow + +If an update fails and leaves your project in a broken state: + +1. **Restore**: Run `npx quartz restore` to bring back your content. +2. **Clean**: Use Git to reset any other broken code files. +3. **Retry**: Attempt the update again or manually apply the changes you need. diff --git a/Local/storage/thlab-notes/worker/docs/cli/sync.md b/Local/storage/thlab-notes/worker/docs/cli/sync.md new file mode 100644 index 0000000..2d27bdb --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/sync.md @@ -0,0 +1,74 @@ +--- +title: quartz sync +--- + +The `sync` command automates the process of pushing your local changes to GitHub and pulling updates from your remote repository. It simplifies the Git workflow for users who want to keep their site updated without running manual Git commands. + +## Flags + +| Flag | Shorthand | Description | Default | +| ------------- | --------- | ------------------------------------ | ----------------- | +| `--directory` | `-d` | The directory of your Quartz project | Current directory | +| `--verbose` | `-v` | Enable detailed logging | `false` | +| `--commit` | | Whether to commit changes | `true` | +| `--no-commit` | | Skip committing changes | `false` | +| `--message` | `-m` | Custom commit message | `update content` | +| `--push` | | Whether to push changes to remote | `true` | +| `--no-push` | | Skip pushing changes | `false` | +| `--pull` | | Whether to pull changes from remote | `true` | +| `--no-pull` | | Skip pulling changes | `false` | + +## Workflow + +When you run `npx quartz sync`, Quartz performs the following steps: + +1. **Pull**: It fetches and merges changes from your remote GitHub repository. +2. **Add**: It stages all new and modified files in your project. +3. **Commit**: It creates a new commit with your changes. +4. **Push**: It sends your new commit to GitHub. + +## Common Workflows + +### Regular Sync + +The most common usage is to simply run the command with no flags. This pulls, commits, and pushes everything. + +```shell +npx quartz sync +``` + +### First Sync + +If you have just set up a new repository and haven't pushed anything yet, you might want to skip the pull step. + +```shell +npx quartz sync --no-pull +``` + +### Custom Commit Message + +You can provide a more descriptive message for your changes. + +```shell +npx quartz sync --message "add new notes about gardening" +``` + +### Sync from Another Device + +If you are working on a different computer and just want to get the latest changes without pushing anything back yet. + +```shell +npx quartz sync --no-push --no-commit +``` + +## Troubleshooting + +### Git Buffer + +If you have a very large number of changes, Git might occasionally fail due to buffer limits. If this happens, try syncing smaller batches of files or increasing your Git post buffer size. + +### Autostash + +Quartz uses `git pull --rebase --autostash` internally. This means if you have unstaged changes when you run `sync`, Quartz will temporarily hide them, pull the remote changes, and then bring your changes back. If a conflict occurs during this process, you will need to resolve it manually using standard Git tools. + +For more information on initial setup, see [[installation]]. diff --git a/Local/storage/thlab-notes/worker/docs/cli/tui.md b/Local/storage/thlab-notes/worker/docs/cli/tui.md new file mode 100644 index 0000000..9b9aa9f --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/tui.md @@ -0,0 +1,64 @@ +--- +title: quartz tui +--- + +The `tui` command launches an interactive terminal user interface for managing your Quartz project. It provides a visual way to manage plugins, arrange your site layout, and edit general settings. + +## Prerequisites + +To use the TUI, you must have the following: + +1. **Bun**: The TUI requires the Bun runtime. You can find installation instructions at [bun.sh](https://bun.sh/docs/installation). +2. **TUI Plugin**: You must install the TUI plugin in your Quartz project. + +### Installation + +Run the following command to add the TUI plugin: + +```shell +npx quartz plugin add github:quartz-community/tui +``` + +## Interface Panels + +The TUI is divided into three main panels that you can navigate between. + +### Plugins Panel + +This panel allows you to browse all available and installed plugins. You can: + +- Enable or disable plugins with a single keystroke. +- Configure plugin-specific settings. +- Install new plugins from the community or remove existing ones. + +### Layout Panel + +The Layout panel is where you define where components appear on your site. You can: + +- Move components between different sections (e.g. `left`, `right`, `beforeBody`). +- Reorder components within a section to change their vertical stack. +- Set priorities for components to control their placement. + +### Settings Panel + +This panel provides a central place to edit your `quartz.config.yaml` settings. You can update: + +- `pageTitle` +- Theme colors and fonts +- Analytics configuration +- Deployment settings + +## Navigation + +The TUI uses standard terminal navigation keys: + +- **Arrow Keys**: Move between items and panels. +- **Enter**: Select an item or confirm a change. +- **Esc**: Go back or cancel an action. +- **Tab**: Cycle through different interface elements. + +## Important Note + +All changes made within the TUI are written directly to your `quartz.config.yaml` file. It is a good practice to have a clean Git state before using the TUI so you can easily review and undo any changes it makes. + +For command-line based plugin management, see [[cli/plugin|quartz plugin]]. diff --git a/Local/storage/thlab-notes/worker/docs/cli/upgrade.md b/Local/storage/thlab-notes/worker/docs/cli/upgrade.md new file mode 100644 index 0000000..8952a6d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/cli/upgrade.md @@ -0,0 +1,48 @@ +--- +title: quartz upgrade +--- + +The `upgrade` command upgrades the Quartz framework itself to the latest version by pulling changes from the official Quartz repository. + +## Usage + +```shell +npx quartz upgrade +``` + +## How it Works + +When you run `npx quartz upgrade`, Quartz performs the following steps: + +1. **Backs up your content** — your content folder is cached locally to prevent data loss. +2. **Pulls the latest Quartz code** — fetches and merges from the official upstream repository (`upstream/v5`) using Git. +3. **Shows version changes** — displays the version transition (e.g., `v5.0.0 → v5.1.0`) or confirms you're already up to date. +4. **Updates dependencies** — runs `npm install` to ensure all packages match the new version. +5. **Restores plugins** — reinstalls plugins from `quartz.lock.json` to ensure compatibility. +6. **Checks plugin compatibility** — verifies that installed plugins are compatible with the new Quartz version. + +## Handling Conflicts + +Because Quartz allows you to customize almost every part of the code, upgrades can sometimes result in merge conflicts. This happens if you have modified a file that the Quartz team has also updated. + +Quartz automatically handles merge conflicts in `quartz.lock.json` by backing up your lockfile before pulling and restoring it afterward. This prevents the most common source of conflicts during upgrades. + +For other files, if a conflict occurs: + +1. Git will mark the conflicting sections in the affected files. +2. You will need to open these files and manually choose which changes to keep. +3. After resolving the conflicts, you can commit the changes. + +## Recovery + +If an upgrade goes wrong or leaves your project in an unusable state, you can use the [[restore|restore]] command to recover your content from the local cache. + +## Flags + +The `upgrade` command supports the standard [[cli/index|common flags]] (`--directory`, `--verbose`). + +## See Also + +- [[cli/plugin|quartz plugin install --latest]] — update installed plugins +- [[upgrading|Upgrading Quartz]] — detailed upgrading guide +- [[restore|quartz restore]] — recover content from cache diff --git a/Local/storage/thlab-notes/worker/docs/community.md b/Local/storage/thlab-notes/worker/docs/community.md new file mode 100644 index 0000000..c14a1c7 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/community.md @@ -0,0 +1,60 @@ +--- +title: Community +--- + +Quartz has a vibrant community of users and contributors. This page highlights community-created plugins, tools, and resources that extend Quartz. + +> [!tip] Contributing +> Know of a great community resource? Submit a pull request to add it to this page! + +## Community Plugins + +Third-party plugins that extend Quartz functionality. Install them with the [[cli/plugin|plugin CLI]]: + +```bash +npx quartz plugin add +``` + + + + +_No community plugins listed yet. Be the first to share yours!_ + +## Tools & Integrations + +Tools, scripts, and integrations built by the community to work with Quartz. + + + + +_No community tools listed yet._ + +## Templates & Themes + +Custom themes, CSS snippets, and starter templates for Quartz sites. + + + + +_No community templates listed yet._ + +## Guides & Tutorials + +Community-written guides, blog posts, and tutorials about using Quartz. + + + + +_No community guides listed yet._ + +## Related Projects + +Projects and tools in the digital garden / PKM ecosystem that pair well with Quartz. + +- **[Obsidian](https://obsidian.md/)** — Knowledge base and note-taking app (recommended editor for Quartz content) + +--- + +Looking to see sites built with Quartz? Check out the [[showcase|Quartz Showcase]]. + +Want to chat with other Quartz users? [Join the Discord community](https://discord.gg/cRFFHYye7t). diff --git a/Local/storage/thlab-notes/worker/docs/configuration.md b/Local/storage/thlab-notes/worker/docs/configuration.md new file mode 100644 index 0000000..3563a08 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/configuration.md @@ -0,0 +1,303 @@ +--- +title: Configuration +--- + +Quartz is meant to be extremely configurable, even if you don't know any coding. Most of the configuration you should need can be done by just editing `quartz.config.yaml`. + +> [!tip] +> If you edit `quartz.config.yaml` using a text-editor with YAML language support like VSCode, it will warn you when you've made an error in your configuration, helping you avoid configuration mistakes! + +The configuration of Quartz can be broken down into two main parts: + +```yaml title="quartz.config.yaml" +configuration: + pageTitle: "My Site" + # ... general configuration +plugins: + - source: github:quartz-community/some-plugin + enabled: true + # ... plugin entries +``` + +## General Configuration + +This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: + +- `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. +- `pageTitleSuffix`: a string added to the end of the page title. This only applies to the browser tab title, not the title shown at the top of the page. +- `enableSPA`: whether to enable [[SPA Routing]] on your site. +- `enablePopovers`: whether to enable [[popover previews]] on your site. +- `analytics`: what to use for analytics on your site. Values can be + - `null`: don't use analytics; + - `{ provider: 'google', tagId: '' }`: use Google Analytics; + - `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: 'https://' }` (self-hosted, make sure to include the `https://` protocol prefix): use [Plausible](https://plausible.io/); + - `{ provider: 'umami', host: '', websiteId: '' }`: use [Umami](https://umami.is/); + - `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id' }` (managed) or `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id', host: 'my-goatcounter-domain.com', scriptSrc: 'https://my-url.to/counter.js' }` (self-hosted) use [GoatCounter](https://goatcounter.com); + - `{ provider: 'posthog', apiKey: '', host: '' }`: use [Posthog](https://posthog.com/); + - `{ provider: 'tinylytics', siteId: '' }`: use [Tinylytics](https://tinylytics.app/); + - `{ provider: 'cabin' }` or `{ provider: 'cabin', host: 'https://cabin.example.com' }` (custom domain): use [Cabin](https://withcabin.com); + - `{provider: 'clarity', projectId: ') patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details. +- `theme`: configure how the site looks. + - `fontOrigin`: where to load fonts from. + - `"googleFonts"` (default): loads fonts from Google Fonts API. Fastest option, especially with CDN caching enabled. + - `"local"`: downloads fonts and serves them from your site. Fully self-contained with no external requests. + - `cdnCaching`: if `true` (default), use Google CDN to cache the fonts. This will generally be faster. Disable (`false`) this if you want Quartz to download the fonts to be self-contained. + - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. + - `title`: font for the title of the site (optional, same as `header` by default) + - `header`: font to use for headers + - `code`: font for inline and block quotes + - `body`: font for everything + - `colors`: controls the theming of the site. + - `light`: page background + - `lightgray`: borders + - `gray`: graph links, heavier borders + - `darkgray`: body text + - `dark`: header text and icons + - `secondary`: link colour, current [[graph view|graph]] node + - `tertiary`: hover states and visited [[graph view|graph]] nodes + - `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]] + - `textHighlight`: markdown highlighted text background + +## Plugins + +You can think of Quartz plugins as a series of transformations over content. + +![[quartz transform pipeline.png]] + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + order: 10 # controls execution order + - source: github:quartz-community/syntax-highlighting + enabled: true + order: 20 + # ... more plugins +``` + +Plugins are categorized by their type (transformer, filter, emitter, pageType) based on their manifest. The `order` field controls execution order within each category. + +> [!note] +> For advanced TS override of plugin configuration, you can modify `quartz.ts`: +> +> ```ts title="quartz.ts" +> import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" +> +> const config = await loadQuartzConfig({ +> // override any configuration field here +> }) +> export default config +> export const layout = await loadQuartzLayout() +> ``` + +- [[tags/plugin/transformer|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description) +- [[tags/plugin/filter|Filters]] **filter** content (e.g. filtering out drafts) +- [[tags/plugin/emitter|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag) +- **Page Types** define how different types of pages are rendered (content pages, folder listings, tag listings). Each page type can use a different [[layout#Page Frames|page frame]] to control its overall HTML structure. + +The `layout.byPageType` section in `quartz.config.yaml` can also set a `template` field to override the page frame for a specific page type: + +```yaml title="quartz.config.yaml" +layout: + byPageType: + canvas: + template: minimal # Override the page frame for canvas pages +``` + +See [[layout#Page Frames]] for details on available frames and how frame resolution works. + +### Internal vs External Plugins + +Quartz distinguishes between internal plugins that are bundled with Quartz and community plugins that are installed separately. + +In `quartz.config.yaml`, community plugins are referenced by their GitHub source: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark +``` + +Internal plugins (like `FrontMatter`) are bundled with Quartz. Community plugins are installed separately and referenced by their `github:org/repo` source. + +### Community Plugins + +To install a community plugin, you can use the following command: + +```shell +npx quartz plugin add github:quartz-community/explorer +``` + +This adds the plugin to `quartz.config.yaml` and installs it to `.quartz/plugins/`. + +To install all plugins referenced in your config that aren't yet installed (useful when cloning a project or setting up CI): + +```shell +npx quartz plugin install --from-config +``` + +To remove installed plugins that are no longer in your config: + +```shell +npx quartz plugin prune +``` + +Both commands support `--dry-run` to preview changes. See [[cli/plugin|the plugin CLI reference]] for full details. + +### Advanced Source Options + +The `source` field for a plugin can be either a simple string or an object with additional options. The string form is the most common: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true +``` + +For plugins that live in a subdirectory of a repository (monorepo-style), or when you need to pin to a specific branch or tag, use the object form: + +```yaml title="quartz.config.yaml" +plugins: + - source: + repo: "https://github.com/user/repo.git" + subdir: plugin + ref: main + name: my-plugin + enabled: true +``` + +The object form supports the following fields: + +| Field | Required | Description | +| -------- | :------: | --------------------------------------------------------------------------------------------------------- | +| `repo` | ✅ | Git repository URL (e.g. `https://github.com/user/repo.git`). | +| `subdir` | ❌ | Subdirectory within the repository that contains the plugin. Used for monorepo-style plugin repositories. | +| `ref` | ❌ | Git ref (branch or tag) to pin to. Equivalent to the `#ref` suffix on string sources. | +| `name` | ❌ | Override the directory name used in `.quartz/plugins/`. Defaults to the repository name. | + +> [!example] Real-world example +> The [quartz-themes](https://github.com/saberzero1/quartz-themes) plugin lives in the `plugin/` subdirectory of its repository. To install it: +> +> ```yaml title="quartz.config.yaml" +> plugins: +> - source: +> name: quartz-themes +> repo: "https://github.com/saberzero1/quartz-themes.git" +> subdir: plugin +> enabled: true +> options: +> theme: "tokyo-night" +> mode: both +> ``` + +> [!tip] +> The string form `github:user/repo#branch` and the object form `{ repo, ref }` are equivalent ways to specify a branch. Use the object form when you also need `subdir` or `name`, or when you prefer a more readable configuration. + +### Usage + +You can customize the behaviour of Quartz by adding, removing and reordering plugins in `quartz.config.yaml`. Each plugin entry specifies its source, whether it's enabled, execution order, and any options: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + order: 5 + - source: github:quartz-community/created-modified-date + enabled: true + options: + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 +``` + +> [!note] +> Some plugin options require JavaScript callback functions (e.g. custom sort, filter, or map functions) that can't be expressed in YAML. For these, use the TS override in `quartz.ts`: +> +> ```ts title="quartz.ts" +> import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" +> import * as ExternalPlugin from "./.quartz/plugins" +> +> ExternalPlugin.Explorer({ +> mapFn: (node) => { +> node.displayName = node.displayName.toUpperCase() +> return node +> }, +> }) +> +> const config = await loadQuartzConfig() +> export default config +> export const layout = await loadQuartzLayout() +> ``` +> +> Options set in `quartz.ts` are merged with YAML options and take precedence. Plugin overrides must be placed **before** `loadQuartzConfig()` so they are applied when components are instantiated during config loading. See the plugin-specific documentation for available callback options. + +You can see a list of all plugins and their configuration options [[tags/plugin|here]]. + +If you'd like to make your own plugins, see the [[making plugins|making custom plugins]] guide. + +## Fonts + +Fonts can be specified as a simple string or with advanced options in `quartz.config.yaml`: + +```yaml title="quartz.config.yaml" +configuration: + theme: + typography: + title: Schibsted Grotesk # optional, defaults to header font + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono +``` + +For more control over font weights and italics, use the TS override in `quartz.ts`: + +```ts title="quartz.ts" +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" + +const config = await loadQuartzConfig({ + theme: { + typography: { + header: { + name: "Schibsted Grotesk", + weights: [400, 700], + includeItalic: true, + }, + body: "Source Sans Pro", + code: "IBM Plex Mono", + }, + }, +}) +export default config +export const layout = await loadQuartzLayout() +``` + +> [!tip] +> For per-heading font control, self-hosted fonts, or Obsidian theme font bridging, see the [[plugins/Fonts|Fonts]] plugin. It can download Google Fonts at build time and serve them locally with `fontOrigin: selfHosted`, making your site fully self-contained. diff --git a/Local/storage/thlab-notes/worker/docs/features/Bases.md b/Local/storage/thlab-notes/worker/docs/features/Bases.md new file mode 100644 index 0000000..4df918b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Bases.md @@ -0,0 +1,21 @@ +--- +title: Bases Support +tags: + - component +--- + +Quartz supports rendering [Obsidian Bases](https://obsidian.md/changelog/2025-04-15-desktop-v1.8.0/) (`.base` files) as interactive database-like views. Bases files define queries over your vault's notes and display the results in configurable views such as tables, lists, cards, galleries, and boards. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +Bases support is provided by the [[BasesPage]] plugin. See the plugin page for configuration options, built-in views, the expression engine, and how to extend with custom views. + +## Demo + +![[Base.base]] + +## Customization + +- Install: `npx quartz plugin add github:quartz-community/bases-page` +- Source: [`quartz-community/bases-page`](https://github.com/quartz-community/bases-page) diff --git a/Local/storage/thlab-notes/worker/docs/features/Canvas.md b/Local/storage/thlab-notes/worker/docs/features/Canvas.md new file mode 100644 index 0000000..f1bda22 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Canvas.md @@ -0,0 +1,21 @@ +--- +title: Canvas Support +tags: + - component +--- + +Quartz supports rendering [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. This brings your Obsidian canvas files to the web, preserving text nodes, file references, link nodes, group nodes, and edges with full visual fidelity. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +Canvas support is provided by the [[CanvasPage]] plugin. See the plugin page for configuration options and a full list of supported features. + +## Demo + +![[Canvas.canvas]] + +## Customization + +- Install: `npx quartz plugin add github:quartz-community/canvas-page` +- Source: [`quartz-community/canvas-page`](https://github.com/quartz-community/canvas-page) diff --git a/Local/storage/thlab-notes/worker/docs/features/Citations.md b/Local/storage/thlab-notes/worker/docs/features/Citations.md new file mode 100644 index 0000000..741590b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Citations.md @@ -0,0 +1,31 @@ +--- +title: Citations +tags: + - feature/transformer +--- + +Quartz uses [rehype-citation](https://github.com/timlrx/rehype-citation) to support parsing of a BibTex bibliography file. + +Under the default configuration, a citation key `[@templeton2024scaling]` will be exported as `(Templeton et al., 2024)`. + +> [!example]- BibTex file +> +> ```bib title="bibliography.bib" +> @article{templeton2024scaling, +> title={Scaling Monosemanticity: Extracting Interpretable Features from Claude 3 Sonnet}, +> author={Templeton, Adly and Conerly, Tom and Marcus, Jonathan and Lindsey, Jack and Bricken, Trenton and Chen, Brian and Pearce, Adam and Citro, Craig and Ameisen, Emmanuel and Jones, Andy and Cunningham, Hoagy and Turner, Nicholas L and McDougall, Callum and MacDiarmid, Monte and Freeman, C. Daniel and Sumers, Theodore R. and Rees, Edward and Batson, Joshua and Jermyn, Adam and Carter, Shan and Olah, Chris and Henighan, Tom}, +> year={2024}, +> journal={Transformer Circuits Thread}, +> url={https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html} +> } +> ``` + +> [!note] Behaviour of references +> +> By default, the references will be included at the end of the file. To control where the references to be included, uses `[^ref]` +> +> Refer to `rehype-citation` docs for more information. + +## Customization + +Citation parsing is a functionality of the [[plugins/Citations|Citation]] plugin. **This plugin is not enabled by default**. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/Docker Support.md b/Local/storage/thlab-notes/worker/docs/features/Docker Support.md new file mode 100644 index 0000000..e64566f --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Docker Support.md @@ -0,0 +1,17 @@ +--- +title: "Docker Support" +tags: + - feature +--- + +Quartz comes shipped with a Docker image that will allow you to preview your Quartz locally without installing Node. + +You can run the below one-liner to run Quartz in Docker. + +```sh +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]]. diff --git a/Local/storage/thlab-notes/worker/docs/features/Latex.md b/Local/storage/thlab-notes/worker/docs/features/Latex.md new file mode 100644 index 0000000..65bf70b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Latex.md @@ -0,0 +1,81 @@ +--- +title: LaTeX +tags: + - feature/transformer +--- + +Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time. + +## Syntax + +### Block Math + +Block math can be rendered by delimiting math expression with `$$`. + +``` +$$ +f(x) = \int_{-\infty}^\infty + f\hat(\xi),e^{2 \pi i \xi x} + \,d\xi +$$ +``` + +$$ +f(x) = \int_{-\infty}^\infty + f\hat(\xi),e^{2 \pi i \xi x} + \,d\xi +$$ + +$$ +\begin{aligned} +a &= b + c \\ &= e + f \\ +\end{aligned} +$$ + +$$ +\begin{bmatrix} +1 & 2 & 3 \\ +a & b & c +\end{bmatrix} +$$ + +$$ +\begin{array}{rll} +E \psi &= H\psi & \text{Expanding the Hamiltonian Operator} \\ +&= -\frac{\hbar^2}{2m}\frac{\partial^2}{\partial x^2} \psi + \frac{1}{2}m\omega x^2 \psi & \text{Using the ansatz $\psi(x) = e^{-kx^2}f(x)$, hoping to cancel the $x^2$ term} \\ +&= -\frac{\hbar^2}{2m} [4k^2x^2f(x)+2(-2kx)f'(x) + f''(x)]e^{-kx^2} + \frac{1}{2}m\omega x^2 f(x)e^{-kx^2} &\text{Removing the $e^{-kx^2}$ term from both sides} \\ +& \Downarrow \\ +Ef(x) &= -\frac{\hbar^2}{2m} [4k^2x^2f(x)-4kxf'(x) + f''(x)] + \frac{1}{2}m\omega x^2 f(x) & \text{Choosing $k=\frac{im}{2}\sqrt{\frac{\omega}{\hbar}}$ to cancel the $x^2$ term, via $-\frac{\hbar^2}{2m}4k^2=\frac{1}{2}m \omega$} \\ +&= -\frac{\hbar^2}{2m} [-4kxf'(x) + f''(x)] \\ +\end{array} +$$ + +> [!warn] +> Due to limitations in the [underlying parsing library](https://github.com/remarkjs/remark-math), block math in Quartz requires the `$$` delimiters to be on newlines like above. + +### Inline Math + +Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$ + +### Escaping symbols + +There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex. + +To get around this, you can escape the dollar sign by doing `\$` instead. + +For example: + +- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2 +- Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2 + +### Using mhchem + +If you are using the community Latex plugin, you can add `mhchem` support by forking the plugin repository and adding the following import to the top of `src/index.ts` (before all the other imports): + +```ts title="src/index.ts" +import "katex/contrib/mhchem" +``` + +## Customization + +Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/Mermaid diagrams.md b/Local/storage/thlab-notes/worker/docs/features/Mermaid diagrams.md new file mode 100644 index 0000000..9cc4089 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Mermaid diagrams.md @@ -0,0 +1,34 @@ +--- +title: "Mermaid Diagrams" +tags: + - feature/transformer +--- + +Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. + +By default, Quartz will render Mermaid diagrams to match the site theme. + +> [!warning] +> Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]]. + +## Syntax + +To add a Mermaid diagram, create a mermaid code block. + +```` +```mermaid +sequenceDiagram + Alice->>+John: Hello John, how are you? + Alice->>+John: John, can you hear me? + John-->>-Alice: Hi Alice, I can hear you! + John-->>-Alice: I feel great! +``` +```` + +```mermaid +sequenceDiagram + Alice->>+John: Hello John, how are you? + Alice->>+John: John, can you hear me? + John-->>-Alice: Hi Alice, I can hear you! + John-->>-Alice: I feel great! +``` diff --git a/Local/storage/thlab-notes/worker/docs/features/Obsidian compatibility.md b/Local/storage/thlab-notes/worker/docs/features/Obsidian compatibility.md new file mode 100644 index 0000000..fff943b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Obsidian compatibility.md @@ -0,0 +1,223 @@ +--- +title: "Obsidian Compatibility" +tags: + - feature/transformer +--- + +Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian. + +By default, Quartz ships with the [[ObsidianFlavoredMarkdown]] plugin, which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]]. + +It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the [[Frontmatter]] transformer plugin. + +Finally, Quartz also provides [[CrawlLinks]] plugin, which allows you to customize Quartz's link resolution behaviour to match Obsidian. + +## Supported Features + +### Wikilinks + +Internal links using the `[[page]]` syntax are converted to regular links. See [[wikilinks]] for more details. All variations are supported: + +```markdown +[[Page]] Link to a page +[[Page|Custom text]] Link with alias +[[Page#Heading]] Link to a heading +[[Page#Heading|Custom text]] Link to a heading with alias +[[Page#^block-id]] Link to a block reference +![[Page]] Embed (transclude) a page +![[image.png]] Embed an image +![[image.png|alt 100x200]] Embed with alt text and dimensions +``` + +Inside tables, pipes in wikilinks can be escaped with a backslash: + +```markdown +| Column | +| --------------- | +| [[page\|alias]] | +``` + +### Highlights + +Wrap text in `==` to highlight it: + +```markdown +This is ==highlighted text== in a sentence. +``` + +This renders as: This is ==highlighted text== in a sentence. + +### Comments + +Obsidian-style comments are stripped from the output: + +```markdown +This is visible. %%This is a comment and won't appear.%% +``` + +This renders as: This is visible. %%This is a comment and won't appear.%% + +Multi-line comments are also supported: + +```markdown +%% +This entire block +is a comment. +%% +``` + +### Tags + +Tags starting with `#` are parsed and linked to tag pages: + +```markdown +#tag #nested/tag #tag-with-dashes +``` + +For example: #feature/transformer + +> [!note] +> Pure numeric tags like `#123` are ignored, matching Obsidian behaviour. + +### Callouts + +[[callouts|Obsidian callouts]] are fully supported, including collapsible variants: + +```markdown +> [!note] +> This is a note callout. + +> [!warning]- Collapsed by default +> This content is hidden initially. + +> [!tip]+ Expanded by default +> This content is visible initially. +``` + +> [!example] Live example +> This is a live callout rendered from Obsidian-flavored Markdown. + +All built-in callout types are supported: `note`, `abstract`, `info`, `todo`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, and `quote`, along with their aliases. + +### Task Lists and Custom Task Characters + +Standard checkboxes work out of the box. With `enableCheckbox: true`, you also get support for custom task characters that are popular in the Obsidian community: + +```markdown +- [ ] Unchecked +- [x] Checked +- [?] Question +- [!] Important +- [>] Forwarded +- [/] In progress +- [-] Cancelled +- [s] Special +``` + +Each custom character is preserved as a `data-task` attribute on the rendered element, allowing CSS-based styling per character. + +- [ ] Unchecked +- [x] Checked +- [?] Question +- [!] Important + +### Mermaid Diagrams + +[[Mermaid diagrams|Mermaid]] code blocks are rendered as diagrams: + +````markdown +```mermaid +graph TD + A[Start] --> B{Decision} + B -->|Yes| C[OK] + B -->|No| D[Cancel] +``` +```` + +```mermaid +graph TD + A[Start] --> B{Decision} + B -->|Yes| C[OK] + B -->|No| D[Cancel] +``` + +### YouTube Embeds + +YouTube videos can be embedded using standard image syntax with a YouTube URL: + +```markdown +![](https://youtu.be/v5LGaczJaf0) +![](https://www.youtube.com/watch?v=v5LGaczJaf0) +``` + +For example, the following embed is rendered from `![](https://youtu.be/v5LGaczJaf0)`: + +![](https://youtu.be/v5LGaczJaf0) + +### Tweet Embeds + +Tweets from Twitter/X are embedded as static blockquotes with a link to the original: + +```markdown +![](https://x.com/kepano/status/1882142872826442145) +![](https://twitter.com/kepano/status/1882142872826442145) +``` + +For example, the following embed is rendered from `![](https://x.com/kepano/status/1882142872826442145)`: + +![](https://x.com/kepano/status/1882142872826442145) + +### Block References + +Block references allow linking to specific blocks within a page: + +```markdown +Content paragraph. ^my-block + +[[Page#^my-block]] +``` + +### Obsidian URI Links + +Links using the `obsidian://` protocol are marked with a CSS class (`obsidian-uri`) and a `data-obsidian-uri` attribute, so you can style them differently from regular links. + +### Video Embeds + +Video files can be embedded using standard image syntax: + +```markdown +![](video.mp4) +![](video.webm) +``` + +### Embed in HTML + +By default, Obsidian does not render its Markdown syntax inside HTML blocks. Quartz extends this with the `enableInHtmlEmbed` option, which parses wikilinks, highlights, and tags inside raw HTML nodes. + +### Footnotes + +Footnotes using the `[^1]` syntax are fully supported through the [[GitHubFlavoredMarkdown]] plugin: + +```markdown +Here is a sentence with a footnote.[^1] + +[^1]: This is the footnote content. +``` + +## Obsidian Community Plugin Support + +Quartz focuses on supporting Obsidian's core features. Functionality from Obsidian community plugins is handled by Quartz community plugins: + +| Obsidian Plugin | Quartz Support | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Dataview | Supported via [Quartz Syncer](https://community.obsidian.md/plugins/quartz-syncer) — exports Dataview queries as static content during sync | +| Excalidraw | Supported via the `obsidian-plugin-excalidraw` community plugin | +| Leaflet Maps | Supported via the `obsidian-plugin-leaflet` community plugin | +| Style Settings | Supported via the `quartz-themes` community plugin | + +> [!tip] +> As a general rule: Obsidian core features are supported by Quartz directly, while Obsidian community plugin features are supported by corresponding Quartz community plugins. Not all Obsidian community plugins will have Quartz equivalents, but popular ones are likely to be supported by the community. + +## Configuration + +This functionality is provided by the [[ObsidianFlavoredMarkdown]], [[Frontmatter]] and [[CrawlLinks]] plugins. See the plugin pages for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/OxHugo compatibility.md b/Local/storage/thlab-notes/worker/docs/features/OxHugo compatibility.md new file mode 100644 index 0000000..6daaffe --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/OxHugo compatibility.md @@ -0,0 +1,48 @@ +--- +title: "OxHugo Compatibility" +tags: + - feature/transformer +--- + +[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. + +Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by the [[OxHugoFlavoredMarkdown]] plugin. Even though this plugin was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown. + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + order: 30 + - source: github:quartz-community/ox-hugo + enabled: true + order: 25 # must come before obsidian-flavored-markdown + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/note-properties + enabled: true + options: + delimiters: "+++" + language: toml # if using toml frontmatter + order: 5 +``` + +For the TS override approach, place overrides before `loadQuartzConfig()` in `quartz.ts`: + +```ts title="quartz.ts (override)" +import * as ExternalPlugin from "./.quartz/plugins" + +ExternalPlugin.NoteProperties({ delims: "+++", language: "toml" }) +ExternalPlugin.OxHugoFlavouredMarkdown() +``` + +> [!note] +> In YAML, plugin execution order is controlled by the `order` field. Lower numbers execute first. Ensure `ox-hugo` has a lower `order` than `obsidian-flavored-markdown`. + +## Usage + +Quartz by default doesn't understand `org-roam` files as they aren't Markdown. You're responsible for using an external tool like `ox-hugo` to export the `org-roam` files as Markdown content to Quartz and managing the static assets so that they're available in the final output. + +## Configuration + +This functionality is provided by the [[OxHugoFlavoredMarkdown]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/RSS Feed.md b/Local/storage/thlab-notes/worker/docs/features/RSS Feed.md new file mode 100644 index 0000000..4b1a1bb --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/RSS Feed.md @@ -0,0 +1,10 @@ +Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly. + +> [!info] +> After deploying, the generated RSS link will be available at `https://${baseUrl}/index.xml` by default. +> +> The `index.xml` path can be customized by passing the `rssSlug` option to the [[ContentIndex]] plugin. + +## Configuration + +This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/Roam Research compatibility.md b/Local/storage/thlab-notes/worker/docs/features/Roam Research compatibility.md new file mode 100644 index 0000000..ec94205 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/Roam Research compatibility.md @@ -0,0 +1,35 @@ +--- +title: "Roam Research Compatibility" +tags: + - feature/transformer +--- + +[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way. + +Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into +regular Markdown via the [[RoamFlavoredMarkdown]] plugin. + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/roam + enabled: true + order: 25 # must come before obsidian-flavored-markdown + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + order: 30 +``` + +For the TS override approach, place overrides before `loadQuartzConfig()` in `quartz.ts`: + +```ts title="quartz.ts (override)" +import * as ExternalPlugin from "./.quartz/plugins" + +ExternalPlugin.RoamFlavoredMarkdown() +``` + +> [!warning] +> In YAML, plugin execution order is controlled by the `order` field. Ensure `roam` has a lower `order` value than `obsidian-flavored-markdown` so it runs first. + +## Customization + +This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/SPA Routing.md b/Local/storage/thlab-notes/worker/docs/features/SPA Routing.md new file mode 100644 index 0000000..d18aa21 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/SPA Routing.md @@ -0,0 +1,13 @@ +--- +title: "SPA Routing" +tags: + - feature +--- + +Single-page-app style rendering. This prevents flashes of unstyled content and improves the smoothness of Quartz. + +Under the hood, this is done by hijacking page navigations and instead fetching the HTML via a `GET` request and then diffing and selectively replacing parts of the page using [micromorph](https://github.com/natemoo-re/micromorph). This allows us to change the content of the page without fully refreshing the page, reducing the amount of content that the browser needs to load. + +## Configuration + +- Disable SPA Routing: set the `enableSPA` field of the [[configuration]] in `quartz.config.yaml` to be `false`. diff --git a/Local/storage/thlab-notes/worker/docs/features/backlinks.md b/Local/storage/thlab-notes/worker/docs/features/backlinks.md new file mode 100644 index 0000000..95df12c --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/backlinks.md @@ -0,0 +1,17 @@ +--- +title: Backlinks +tags: + - component +--- + +A backlink for a note is a link from another note to that note. Links in the backlink pane also feature rich [[popover previews]] if you have that feature enabled. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Customization + +- Removing backlinks: remove the `backlinks` entry from `quartz.config.yaml` or set `enabled: false`. +- Hide when empty: hide `Backlinks` if given page doesn't contain any backlinks (default to `true`). To disable this, set `hideWhenEmpty: false` in the plugin options in `quartz.config.yaml`. +- Install: `npx quartz plugin add github:quartz-community/backlinks` +- Source: [`quartz-community/backlinks`](https://github.com/quartz-community/backlinks) diff --git a/Local/storage/thlab-notes/worker/docs/features/breadcrumbs.md b/Local/storage/thlab-notes/worker/docs/features/breadcrumbs.md new file mode 100644 index 0000000..3bb770d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/breadcrumbs.md @@ -0,0 +1,54 @@ +--- +title: "Breadcrumbs" +tags: + - component +--- + +Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders. + +By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!). + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Customization + +Most configuration can be done via the `options` section of the breadcrumbs plugin entry in `quartz.config.yaml`. + +For example, here's what the default configuration looks like: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/breadcrumbs + enabled: true + options: + spacerSymbol: "❯" + rootName: Home + resolveFrontmatterTitle: true + showCurrentPage: true + layout: + position: beforeBody + priority: 5 +``` + +For the TS override approach: + +```ts title="quartz.ts (override)" +// Must be placed before loadQuartzConfig() +ExternalPlugin.Breadcrumbs({ + spacerSymbol: "❯", + rootName: "Home", + resolveFrontmatterTitle: true, + showCurrentPage: true, +}) +``` + +When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. + +You can also adjust where the breadcrumbs will be displayed by changing the `layout.position` field in the plugin entry in `quartz.config.yaml` (see [[layout]]). + +Want to customize it even more? + +- Removing breadcrumbs: remove the `breadcrumbs` entry from `quartz.config.yaml` or set `enabled: false`. +- Install: `npx quartz plugin add github:quartz-community/breadcrumbs` +- Source: [`quartz-community/breadcrumbs`](https://github.com/quartz-community/breadcrumbs) diff --git a/Local/storage/thlab-notes/worker/docs/features/callouts.md b/Local/storage/thlab-notes/worker/docs/features/callouts.md new file mode 100644 index 0000000..4caeeb4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/callouts.md @@ -0,0 +1,96 @@ +--- +title: Callouts +tags: + - feature/transformer +--- + +Quartz supports the same Admonition-callout syntax as Obsidian. + +This includes + +- 12 Distinct callout types (each with several aliases) +- Collapsable callouts + +``` +> [!info] Title +> This is a callout! +``` + +See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts). + +> [!warning] +> Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]]. + +## Customization + +The callouts are a functionality of the [[ObsidianFlavoredMarkdown]] plugin. See the plugin page for how to enable or disable them. + +You can edit the icons by customizing `quartz/styles/callouts.scss`. + +### Add custom callouts + +By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`. + +```scss title="quartz/styles/custom.scss" +.callout { + &[data-callout="custom"] { + --color: #customcolor; + --border: #custombordercolor; + --bg: #custombg; + --callout-icon: url("data:image/svg+xml; utf8, "); //SVG icon code + } +} +``` + +> [!warning] +> Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that. + +## Showcase + +> [!info] +> Default title + +> [!question]+ Can callouts be _nested_? +> +> > [!todo]- Yes!, they can. And collapsed! +> > +> > > [!example] You can even use multiple layers of nesting. + +> [!note] +> Aliases: "note" + +> [!abstract] +> Aliases: "abstract", "summary", "tldr" + +> [!info] +> Aliases: "info" + +> [!todo] +> Aliases: "todo" + +> [!tip] +> Aliases: "tip", "hint", "important" + +> [!success] +> Aliases: "success", "check", "done" + +> [!question] +> Aliases: "question", "help", "faq" + +> [!warning] +> Aliases: "warning", "attention", "caution" + +> [!failure] +> Aliases: "failure", "missing", "fail" + +> [!danger] +> Aliases: "danger", "error" + +> [!bug] +> Aliases: "bug" + +> [!example] +> Aliases: "example" + +> [!quote] +> Aliases: "quote", "cite" diff --git a/Local/storage/thlab-notes/worker/docs/features/comments.md b/Local/storage/thlab-notes/worker/docs/features/comments.md new file mode 100644 index 0000000..c588bae --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/comments.md @@ -0,0 +1,182 @@ +--- +title: Comments +tags: + - component +--- + +Quartz also has the ability to hook into various providers to enable readers to leave comments on your site. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +![[giscus-example.png]] + +As of today, only [Giscus](https://giscus.app/) is supported out of the box but PRs to support other providers are welcome! + +## Providers + +### Giscus + +First, make sure that the [[setting up your GitHub repository|GitHub]] repository you are using for your Quartz meets the following requirements: + +1. The **repository is [public](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/setting-repository-visibility#making-a-repository-public)**, otherwise visitors will not be able to view the discussion. +2. The **[giscus](https://github.com/apps/giscus) app is installed**, otherwise visitors will not be able to comment and react. +3. The **Discussions feature is turned on** by [enabling it for your repository](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository). + +Then, use the [Giscus site](https://giscus.app/#repository) to figure out what your `repoId` and `categoryId` should be. Make sure you select `Announcements` for the Discussion category. + +![[giscus-repo.png]] + +![[giscus-discussion.png]] + +After entering both your repository and selecting the discussion category, Giscus will compute some IDs that you'll need to provide back to Quartz. You won't need to manually add the script yourself as Quartz will handle that part for you but will need these values in the next step! + +![[giscus-results.png]] + +Finally, in `quartz.config.yaml`, add the comments plugin with the following options (using the values you got from above): + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/comments + enabled: true + options: + provider: giscus + options: + repo: jackyzha0/quartz + repoId: MDEwOlJlcG9zaXRvcnkzODcyMTMyMDg + category: Announcements + categoryId: DIC_kwDOFxRnmM4B-Xg6 + lang: en + layout: + position: afterBody + priority: 10 +``` + +For the TS override approach: + +```ts title="quartz.ts (override)" +// If using quartz.ts overrides instead of YAML: +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout({ + defaults: { + afterBody: [ + ExternalPlugin.Comments({ + provider: "giscus", + options: { + repo: "jackyzha0/quartz", + repoId: "MDEwOlJlcG9zaXRvcnkzODcyMTMyMDg", + category: "Announcements", + categoryId: "DIC_kwDOFxRnmM4B-Xg6", + lang: "en", + }, + }), + ], + }, +}) +``` + +> [!note] +> Install the comments plugin first: `npx quartz plugin add github:quartz-community/comments` + +### Customization + +Quartz also exposes a few of the other Giscus options as well and you can provide them the same way `repo`, `repoId`, `category`, and `categoryId` are provided. + +```ts +type Options = { + provider: "giscus" + options: { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + + // Url to folder with custom themes + // defaults to 'https://${cfg.baseUrl}/static/giscus' + themeUrl?: string + + // filename for light theme .css file + // defaults to 'light' + lightTheme?: string + + // filename for dark theme .css file + // defaults to 'dark' + darkTheme?: string + + // how to map pages -> discussions + // defaults to 'url' + mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + + // use strict title matching + // defaults to true + strict?: boolean + + // whether to enable reactions for the main post + // defaults to true + reactionsEnabled?: boolean + + // where to put the comment input box relative to the comments + // defaults to 'bottom' + inputPosition?: "top" | "bottom" + + // set your preference language here + // defaults to 'en' + lang?: string + } +} +``` + +#### Custom CSS theme + +Quartz supports custom theme for Giscus. To use a custom CSS theme, place the `.css` file inside the `quartz/static` folder and set the configuration values. + +For example, if you have a light theme `light-theme.css`, a dark theme `dark-theme.css`, and your Quartz site is hosted at `https://example.com/`: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/comments + enabled: true + options: + provider: giscus + options: + # Other options... + themeUrl: "https://example.com/static/giscus" # corresponds to quartz/static/giscus/ + lightTheme: light-theme # corresponds to light-theme.css in quartz/static/giscus/ + darkTheme: dark-theme # corresponds to dark-theme.css in quartz/static/giscus/ +``` + +```ts title="quartz.ts (override)" +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout({ + defaults: { + afterBody: [ + ExternalPlugin.Comments({ + provider: "giscus", + options: { + // Other options... + themeUrl: "https://example.com/static/giscus", + lightTheme: "light-theme", + darkTheme: "dark-theme", + }, + }), + ], + }, +}) +``` + +#### Conditionally display comments + +Quartz can conditionally display the comment box based on a field `comments` in the frontmatter. By default, all pages will display comments, to disable it for a specific page, set `comments` to `false`. + +``` +--- +title: Comments disabled here! +comments: false +--- +``` diff --git a/Local/storage/thlab-notes/worker/docs/features/darkmode.md b/Local/storage/thlab-notes/worker/docs/features/darkmode.md new file mode 100644 index 0000000..25e1d4d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/darkmode.md @@ -0,0 +1,25 @@ +--- +title: "Darkmode" +tags: + - component +--- + +Quartz supports darkmode out of the box that respects the user's theme preference. Any future manual toggles of the darkmode switch will be saved in the browser's local storage so it can be persisted across future page loads. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Customization + +- Removing darkmode: remove the `darkmode` entry from `quartz.config.yaml` or set `enabled: false`. +- Install: `npx quartz plugin add github:quartz-community/darkmode` +- Source: [`quartz-community/darkmode`](https://github.com/quartz-community/darkmode) + +You can also listen to the `themechange` event to perform any custom logic when the theme changes. + +```js +document.addEventListener("themechange", (e) => { + console.log("Theme changed to " + e.detail.theme) // either "light" or "dark" + // your logic here +}) +``` diff --git a/Local/storage/thlab-notes/worker/docs/features/explorer.md b/Local/storage/thlab-notes/worker/docs/features/explorer.md new file mode 100644 index 0000000..bba7f50 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/explorer.md @@ -0,0 +1,294 @@ +--- +title: "Explorer" +tags: + - component +--- + +Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and is highly customizable. + +> [!info] +> The Explorer is now a community plugin. This demonstrates how external plugins can extend Quartz functionality while serving as a reference implementation for plugin developers. + +## Installation + +The Explorer is available as a community plugin from GitHub: + +```bash +npm install github:quartz-community/explorer --legacy-peer-deps +``` + +Then add it to your `quartz.config.yaml`: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 +``` + +## Features + +By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. + +Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead. + +> [!info] +> The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages. +> +> To clear/delete the explorer state from local storage, delete the `fileTree` entry (guide on how to delete a key from local storage in chromium based browsers can be found [here](https://docs.devolutions.net/kb/general-knowledge-base/clear-browser-local-storage/clear-chrome-local-storage/)). You can disable this by passing `useSavedState: false` as an argument. + +## Customization + +Most configuration can be done by passing in options to `Explorer()`. + +For example, here's what the default configuration looks like: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true + options: + title: Explorer + folderClickBehavior: collapse # "link" to navigate or "collapse" to toggle + folderDefaultState: collapsed # "collapsed" or "open" + useSavedState: true + layout: + position: left + priority: 50 +``` + +For advanced options like custom sort, filter, and map functions, use the TS override in `quartz.ts`: + +```ts title="quartz.ts" +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" +import * as ExternalPlugin from "./.quartz/plugins" + +// Advanced: pass callback functions that can't be expressed in YAML +ExternalPlugin.Explorer({ + sortFn: (a, b) => { + /* ... */ + }, + filterFn: (node) => { + /* ... */ + }, + mapFn: (node) => { + /* ... */ + }, + order: ["filter", "map", "sort"], +}) + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout() +``` + +> [!info] How overrides work +> When you call `ExternalPlugin.Explorer({...})` in `quartz.ts`, the options are recorded and merged with the YAML configuration when the component is instantiated during the build. Options set in `quartz.ts` take precedence over those in `quartz.config.yaml`, following this order: `plugin defaults < YAML options < quartz.ts overrides`. +> +> If you have two plugins that export the same name (e.g. two different Explorer plugins installed via `--name`), use the `plugins` map to disambiguate: +> +> ```ts title="quartz.ts" +> import * as ExternalPlugin from "./.quartz/plugins" +> ExternalPlugin.plugins["my-explorer"].Explorer({ mapFn: ... }) +> ``` + +When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. + +Want to customize it even more? + +- Removing explorer: remove the `explorer` entry from `quartz.config.yaml` or set `enabled: false` + - (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout +- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]] + +## Advanced customization + +This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function. +All functions you can pass work with the `FileTrieNode` class, which has the following properties: + +```ts title="@quartz-community/explorer" +class FileTrieNode { + isFolder: boolean + children: Array + data: ContentDetails | null +} +``` + +```ts +export type ContentDetails = { + slug: FullSlug + title: string + links: SimpleSlug[] + tags: string[] + content: string +} +``` + +Every function you can pass is optional. By default, only a `sort` function will be used: + +```ts title="Default sort function" +// Sort order: folders first, then files. Sort folders and files alphabetically +ExternalPlugin.Explorer({ + sortFn: (a, b) => { + if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) { + return a.displayName.localeCompare(b.displayName, undefined, { + numeric: true, + sensitivity: "base", + }) + } + + if (!a.isFolder && b.isFolder) { + return 1 + } else { + return -1 + } + }, +}) +``` + +--- + +You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one. + +For more information on how to use `sort`, `filter` and `map`, you can check [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) and [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). + +Type definitions look like this: + +```ts +type SortFn = (a: FileTrieNode, b: FileTrieNode) => number +type FilterFn = (node: FileTrieNode) => boolean +type MapFn = (node: FileTrieNode) => void +``` + +## Basic examples + +These examples show the basic usage of `sort`, `map` and `filter`. + +### Use `sort` to put files first + +Using this example, the explorer will alphabetically sort everything. + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true + options: + # Simple options go in YAML + title: Explorer + folderDefaultState: collapsed +``` + +Custom sort functions require the TS override: + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + sortFn: (a, b) => { + return a.displayName.localeCompare(b.displayName) + }, +}) +``` + +### Change display names (`map`) + +Using this example, the display names of all `FileNodes` (folders + files) will be converted to full upper case. + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + mapFn: (node) => { + node.displayName = node.displayName.toUpperCase() + return node + }, +}) +``` + +> [!note] +> The `mapFn`, `filterFn`, and `sortFn` options require JavaScript callback functions and cannot be expressed in YAML. Use the TS override for these. + +### Remove list of elements (`filter`) + +Using this example, you can remove elements from your explorer by providing an array of folders/files to exclude. +Note that this example filters on the title but you can also do it via slug or any other field available on `FileTrieNode`. + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + filterFn: (node) => { + // set containing names of everything you want to filter out + const omit = new Set(["authoring content", "tags", "advanced"]) + + // can also use node.slug or by anything on node.data + // note that node.data is only present for files that exist on disk + // (e.g. implicit folder nodes that have no associated index.md) + return !omit.has(node.displayName.toLowerCase()) + }, +}) +``` + +### Remove files by tag + +You can access the tags of a file by `node.data.tags`. + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + filterFn: (node) => { + // exclude files with the tag "explorerexclude" + return node.data?.tags?.includes("explorerexclude") !== true + }, +}) +``` + +### Show every element in explorer + +By default, the explorer will filter out the `tags` folder. +To override the default filter function, you can set the filter function to `undefined`. + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + filterFn: undefined, // apply no filter function, every file and folder will visible +}) +``` + +## Advanced examples + +> [!tip] +> When writing more complicated functions, the `quartz.ts` file can start to look very cramped. +> You can fix this by defining your sort functions outside of the component +> and passing it in. +> +> ```ts title="quartz.ts" +> import * as ExternalPlugin from "./.quartz/plugins" +> import type { ExplorerOptions } from "./.quartz/plugins" +> +> const mapFn: ExplorerOptions["mapFn"] = (node) => { +> // implement your function here +> } +> const filterFn: ExplorerOptions["filterFn"] = (node) => { +> // implement your function here +> } +> const sortFn: ExplorerOptions["sortFn"] = (a, b) => { +> // implement your function here +> } +> +> ExternalPlugin.Explorer({ +> // ... your other options +> mapFn, +> filterFn, +> sortFn, +> }) +> ``` + +### Add emoji prefix + +To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function in `quartz.ts`: + +```ts title="quartz.ts (override)" +ExternalPlugin.Explorer({ + mapFn: (node) => { + if (node.isFolder) { + node.displayName = "📁 " + node.displayName + } else { + node.displayName = "📄 " + node.displayName + } + }, +}) +``` diff --git a/Local/storage/thlab-notes/worker/docs/features/folder and tag listings.md b/Local/storage/thlab-notes/worker/docs/features/folder and tag listings.md new file mode 100644 index 0000000..3190709 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/folder and tag listings.md @@ -0,0 +1,33 @@ +--- +title: Folder and Tag Listings +tags: + - feature/emitter +--- + +Quartz emits listing pages for any folders and tags you have. + +## Folder Listings + +Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep. + +Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`. + +You can link to the folder listing by referencing its name, plus a trailing slash, like this: `[[advanced/]]` (results in [[advanced/]]). + +By default, Quartz will title the page `Folder: ` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the folder description. + +For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it. + +## Tag Listings + +Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag. + +Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each level of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz. + +You can link to the tag listing by referencing its name with a `tag/` prefix, like this: `[[tags/plugin]]` (results in [[tags/plugin]]). + +As with folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description. + +## Customization + +Quartz allows you to define a custom sort ordering for content on both page types. The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/full-text search.md b/Local/storage/thlab-notes/worker/docs/features/full-text search.md new file mode 100644 index 0000000..6ba4e77 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/full-text search.md @@ -0,0 +1,31 @@ +--- +title: Full-text Search +tags: + - component +--- + +Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +It can be opened by either clicking on the search bar or pressing `⌘`/`ctrl` + `K`. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. + +To search content by tags, you can either press `⌘`/`ctrl` + `shift` + `K` or start your query with `#` (e.g. `#components`). + +This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). You are also able to navigate search results using `ArrowUp` and `ArrowDown`. + +> [!info] +> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. + +### Indexing Behaviour + +By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed. + +It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title, content and tags, weighing title matches above content matches. + +## Customization + +- Removing search: remove the `search` entry from `quartz.config.yaml` or set `enabled: false`. +- Install: `npx quartz plugin add github:quartz-community/search` +- Source: [`quartz-community/search`](https://github.com/quartz-community/search) diff --git a/Local/storage/thlab-notes/worker/docs/features/graph view.md b/Local/storage/thlab-notes/worker/docs/features/graph view.md new file mode 100644 index 0000000..37540d4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/graph view.md @@ -0,0 +1,91 @@ +--- +title: "Graph View" +tags: + - component +--- + +Quartz features a graph-view that can show both a local graph view and a global graph view. + +- The local graph view shows files that either link to the current file or are linked from the current file. In other words, it shows all notes that are _at most_ one hop away. +- The global graph view can be toggled by clicking the graph icon on the top-right of the local graph view. It shows _all_ the notes in your graph and how they connect to each other. + +> [!info] +> The Graph View is now a community plugin. This demonstrates how external plugins can extend Quartz functionality while serving as a reference implementation for plugin developers. + +## Installation + +The Graph View is available as a community plugin from GitHub: + +```bash +npm install github:quartz-community/graph --legacy-peer-deps +``` + +Then add it to your `quartz.config.yaml`: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 +``` + +## Features + +By default, the node radius is proportional to the total number of incoming and outgoing internal links from that file. + +Additionally, similar to how browsers highlight visited links a different colour, the graph view will also show nodes that you have visited in a different colour. + +> [!info] +> Graph View requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. + +## Customization + +Most configuration can be done by passing in options to `Graph()`. + +For example, here's what the default configuration looks like: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/graph + enabled: true + options: + localGraph: + drag: true + zoom: true + depth: 1 + scale: 1.1 + repelForce: 0.5 + centerForce: 0.3 + linkDistance: 30 + fontSize: 0.6 + opacityScale: 1 + removeTags: [] + showTags: true + enableRadial: false + globalGraph: + drag: true + zoom: true + depth: -1 + scale: 0.9 + repelForce: 0.5 + centerForce: 0.3 + linkDistance: 30 + fontSize: 0.6 + opacityScale: 1 + removeTags: [] + showTags: true + focusOnHover: true + enableRadial: true + layout: + position: right + priority: 10 +``` + +When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. + +Want to customize it even more? + +- Removing graph view: remove the `graph` entry from `quartz.config.yaml` or set `enabled: false` +- Component source: https://github.com/quartz-community/graph diff --git a/Local/storage/thlab-notes/worker/docs/features/i18n.md b/Local/storage/thlab-notes/worker/docs/features/i18n.md new file mode 100644 index 0000000..58f72da --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/i18n.md @@ -0,0 +1,18 @@ +--- +title: Internationalization +--- + +Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.yaml`. + +The locale field generally follows a certain format: `{language}-{REGION}` + +- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). +- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + +> [!tip] Interested in contributing? +> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v5/quartz/i18n/locales)! To contribute a translation, do the following things: +> +> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. +> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. +> 3. Fill in the translations! +> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. diff --git a/Local/storage/thlab-notes/worker/docs/features/index.md b/Local/storage/thlab-notes/worker/docs/features/index.md new file mode 100644 index 0000000..773a4c9 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/index.md @@ -0,0 +1,48 @@ +--- +title: Feature List +--- + +Quartz comes with a wide variety of features out of the box. Most features are powered by [[configuration#Plugins|plugins]] that can be configured, enabled, or disabled via `quartz.config.yaml`. + +## Content Features + +- [[Obsidian compatibility]] — Full support for Obsidian-flavored Markdown +- [[wikilinks]] — Link between notes using `[[wikilinks]]` syntax +- [[callouts]] — Obsidian-style callout blocks +- [[features/Latex|Latex]] — LaTeX math rendering +- [[Mermaid diagrams]] — Diagram support via Mermaid +- [[syntax highlighting|Syntax highlighting]] — Code block highlighting with themes +- [[OxHugo compatibility]] — Support for ox-hugo Markdown +- [[Roam Research compatibility]] — Support for Roam Research syntax +- [[features/Citations|Citations]] — Academic citation support +- [[Canvas]] — Render Obsidian Canvas files as interactive pages +- [[Bases]] — Database-like views for your notes (tables, cards, galleries, and more) + +## Navigation & Discovery + +- [[full-text search]] — Search across all your notes +- [[graph view]] — Interactive graph visualization of note connections +- [[features/backlinks]] — See which notes link to the current page +- [[features/explorer]] — File tree sidebar for browsing notes +- [[features/breadcrumbs]] — Breadcrumb navigation trail +- [[table of contents]] — Per-page table of contents +- [[folder and tag listings]] — Browse notes by folder or tag +- [[recent notes]] — Display recently modified notes +- [[popover previews]] — Hover previews for internal links +- [[StackedPages|stacked pages]] — Andy Matuschak-style stacked sliding panes for tracing note connections +- [[EncryptedPages|encrypted pages]] — Password-protect individual pages with client-side encryption + +## Appearance & Reading + +- [[features/darkmode]] — Light and dark mode toggle +- [[reader mode]] — Distraction-free reading experience +- [[features/comments|comments]] — Add comments via Giscus, Utterances, or other providers +- [[social images]] — Auto-generated Open Graph images for social sharing + +## Publishing & Deployment + +- [[RSS Feed]] — RSS feed generation for content syndication +- [[private pages]] — Control which pages are published +- [[SPA Routing]] — Single-page app navigation +- [[Docker Support]] — Build and deploy with Docker +- [[i18n]] — Internationalization with 30+ supported locales diff --git a/Local/storage/thlab-notes/worker/docs/features/popover previews.md b/Local/storage/thlab-notes/worker/docs/features/popover previews.md new file mode 100644 index 0000000..bd128e2 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/popover previews.md @@ -0,0 +1,17 @@ +--- +title: Popover Previews +--- + +Like Wikipedia, when you hover over a link in Quartz, there is a popup of a page preview that you can scroll to see the entire content. Links to headers will also scroll the popup to show that specific header in view. + +By default, Quartz only fetches previews for pages inside your vault due to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). It does this by selecting all HTML elements with the `popover-hint` class. For most pages, this includes the page title, page metadata like words and time to read, tags, and the actual page content. + +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-desktop.png|images referenced using wikilinks]] can also be viewed as popups. + +## Configuration + +- Remove popovers: set the `enablePopovers` field in `quartz.config.yaml` to be `false`. +- Style: `quartz/components/styles/popover.scss` +- Script: `quartz/components/scripts/popover.inline.ts` diff --git a/Local/storage/thlab-notes/worker/docs/features/private pages.md b/Local/storage/thlab-notes/worker/docs/features/private pages.md new file mode 100644 index 0000000..bb296af --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/private pages.md @@ -0,0 +1,33 @@ +--- +title: Private Pages +tags: + - feature/filter +--- + +There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction: + +## Filter Plugins + +[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the [[RemoveDrafts]] plugin which filters out any note that has `draft: true` in the frontmatter. + +If you'd like to only publish a select number of notes, you can instead use [[ExplicitPublish]] which will filter out all notes except for any that have `publish: true` in the frontmatter. + +> [!warning] +> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. + +## `ignorePatterns` + +This is a field in `quartz.config.yaml` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [fast-glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here. + +> [!note] +> Bash's glob syntax is slightly different from fast-glob's and using bash's syntax may lead to unexpected results. + +Common examples include: + +- `some/folder`: exclude the entire of `some/folder` +- `*.md`: exclude all files with a `.md` extension +- `!(*.md)` exclude all files that _don't_ have a `.md` extension. Note that negations _must_ parenthesize the rest of the pattern! +- `**/private`: exclude any files or folders named `private` at any level of nesting + +> [!warning] +> Marking something as private via either a plugin or through the `ignorePatterns` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information. diff --git a/Local/storage/thlab-notes/worker/docs/features/reader mode.md b/Local/storage/thlab-notes/worker/docs/features/reader mode.md new file mode 100644 index 0000000..a8f0377 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/reader mode.md @@ -0,0 +1,57 @@ +--- +title: Reader Mode +tags: + - component +--- + +Reader Mode is a feature that allows users to focus on the content by hiding the sidebars and other UI elements. When enabled, it provides a clean, distraction-free reading experience. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Configuration + +Reader Mode is enabled by default. To disable it, set `enabled: false` in your `quartz.config.yaml`: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/reader-mode + enabled: false +``` + +Or remove the plugin entirely: + +```bash +npx quartz plugin remove github:quartz-community/reader-mode +``` + +- Install: `npx quartz plugin add github:quartz-community/reader-mode` +- Source: [`quartz-community/reader-mode`](https://github.com/quartz-community/reader-mode) + +## Usage + +The Reader Mode toggle appears as a button with a book icon. When clicked: + +- Sidebars are hidden +- Hovering over the content area reveals the sidebars temporarily + +Unlike Dark Mode, Reader Mode state is not persisted between page reloads but is maintained during SPA navigation within the site. + +## Customization + +You can customize the appearance of Reader Mode through CSS variables and styles. The component uses the following classes: + +- `.readermode`: The toggle button +- `.readerIcon`: The book icon +- `[reader-mode="on"]`: Applied to the root element when Reader Mode is active + +Example customization in your custom CSS: + +```scss +.readermode { + // Customize the button + svg { + stroke: var(--custom-color); + } +} +``` diff --git a/Local/storage/thlab-notes/worker/docs/features/recent notes.md b/Local/storage/thlab-notes/worker/docs/features/recent notes.md new file mode 100644 index 0000000..f75124b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/recent notes.md @@ -0,0 +1,24 @@ +--- +title: Recent Notes +tags: component +--- + +Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by installing the plugin and configuring it in `quartz.config.yaml`. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Customization + +Most options are configured in the `options` section of the plugin entry in `quartz.config.yaml`: + +- Changing the title from "Recent notes": set `title: "Recent writing"` in options +- Changing the number of recent notes: set `limit: 5` in options +- Display the note's tags (defaults to true): set `showTags: false` in options +- Hide generated tag pages from the list (defaults to false): set `hideTagPages: true` in options. This filters out any page whose slug lives under the conventional `tags/` prefix. +- Hide generated folder index pages from the list (defaults to false): set `hideFolderPages: true` in options. This filters out any page whose slug matches Quartz's folder-path convention (trailing slash or `index` suffix). +- Show a 'see more' link: set `linkToMore: "tags/components"` in options. This field should be a full slug to a page that exists. +- Customize filtering: requires a TS override — pass `filter: someFilterFunction` to the plugin constructor in `quartz.ts`. The filter function should have the signature `(f: QuartzPluginData) => boolean`. +- Customize sorting: requires a TS override — pass `sort: someSortFunction` to the plugin constructor in `quartz.ts`. By default, Quartz will sort by date and then tie break lexographically. The sort function should have the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. +- Install: `npx quartz plugin add github:quartz-community/recent-notes` +- Source: [`quartz-community/recent-notes`](https://github.com/quartz-community/recent-notes) diff --git a/Local/storage/thlab-notes/worker/docs/features/social images.md b/Local/storage/thlab-notes/worker/docs/features/social images.md new file mode 100644 index 0000000..d7f85b1 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/social images.md @@ -0,0 +1,19 @@ +--- +title: "Social Media Preview Cards" +--- + +A lot of social media platforms can display a rich preview for your website when sharing a link (most notably, a cover image, a title and a description). + +Quartz can also dynamically generate and use new cover images for every page to be used in link previews on social media for you. + +## Showcase + +After enabling the [[CustomOgImages]] emitter plugin, the social media link preview for [[authoring content | Authoring Content]] looks like this: + +| Light | Dark | +| ----------------------------------- | ---------------------------------- | +| ![[social-image-preview-light.png]] | ![[social-image-preview-dark.png]] | + +## Configuration + +This functionality is provided by the [[CustomOgImages]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/syntax highlighting.md b/Local/storage/thlab-notes/worker/docs/features/syntax highlighting.md new file mode 100644 index 0000000..bf9baae --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/syntax highlighting.md @@ -0,0 +1,143 @@ +--- +title: Syntax Highlighting +tags: + - feature/transformer +--- + +Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting. + +And, unlike some client-side highlighters, it has a full TextMate parser grammar instead of using Regexes, allowing for highly accurate code highlighting. + +In short, it generates HTML that looks exactly like your code in an editor like VS Code. Under the hood, it's powered by [Rehype Pretty Code](https://rehype-pretty-code.netlify.app/) which uses [Shiki](https://github.com/shikijs/shiki). + +> [!warning] +> Syntax highlighting does have an impact on build speed if you have a lot of code snippets in your notes. + +## Formatting + +Text inside `backticks` on a line will be formatted like code. + +```` +```ts +export function trimPathSuffix(fp: string): string { + fp = clientSideSlug(fp) + let [cleanPath, anchor] = fp.split("#", 2) + anchor = anchor === undefined ? "" : "#" + anchor + + return cleanPath + anchor +} +``` +```` + +```ts +export function trimPathSuffix(fp: string): string { + fp = clientSideSlug(fp) + let [cleanPath, anchor] = fp.split("#", 2) + anchor = anchor === undefined ? "" : "#" + anchor + + return cleanPath + anchor +} +``` + +### Titles + +Add a file title to your code block, with text inside double quotes (`""`): + +```` +```js title="..." + +``` +```` + +```ts title="quartz/path.ts" +export function trimPathSuffix(fp: string): string { + fp = clientSideSlug(fp) + let [cleanPath, anchor] = fp.split("#", 2) + anchor = anchor === undefined ? "" : "#" + anchor + + return cleanPath + anchor +} +``` + +### Line highlighting + +Place a numeric range inside `{}`. + +```` +```js {1-3,4} + +``` +```` + +```ts {2-3,6} +export function trimPathSuffix(fp: string): string { + fp = clientSideSlug(fp) + let [cleanPath, anchor] = fp.split("#", 2) + anchor = anchor === undefined ? "" : "#" + anchor + + return cleanPath + anchor +} +``` + +### Word highlighting + +A series of characters, like a literal regex. + +```` +```js /useState/ +const [age, setAge] = useState(50); +const [name, setName] = useState('Taylor'); +``` +```` + +```js /useState/ +const [age, setAge] = useState(50) +const [name, setName] = useState("Taylor") +``` + +### Inline Highlighting + +Append {:lang} to the end of inline code to highlight it like a regular code block. + +``` +This is an array `[1, 2, 3]{:js}` of numbers 1 through 3. +``` + +This is an array `[1, 2, 3]{:js}` of numbers 1 through 3. + +### Line numbers + +Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`: + +```` +```js showLineNumbers{number} + +``` +```` + +```ts showLineNumbers{20} +export function trimPathSuffix(fp: string): string { + fp = clientSideSlug(fp) + let [cleanPath, anchor] = fp.split("#", 2) + anchor = anchor === undefined ? "" : "#" + anchor + + return cleanPath + anchor +} +``` + +### Escaping code blocks + +You can format a codeblock inside of a codeblock by wrapping it with another level of backtick fences that has one more backtick than the previous fence. + +````` +```` +```js /useState/ +const [age, setAge] = useState(50); +const [name, setName] = useState('Taylor'); +``` +```` +````` + +## Customization + +Syntax highlighting is a functionality of the [[SyntaxHighlighting]] plugin. See the plugin page for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/features/table of contents.md b/Local/storage/thlab-notes/worker/docs/features/table of contents.md new file mode 100644 index 0000000..4ecccc9 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/table of contents.md @@ -0,0 +1,18 @@ +--- +title: "Table of Contents" +tags: + - component + - feature/transformer +--- + +Quartz can automatically generate a table of contents (TOC) from a list of headings on each page. It will also show you your current scrolling position on the page by highlighting headings you've scrolled through with a different color. + +You can hide the TOC on a page by adding `enableToc: false` to the frontmatter for that page. + +By default, the TOC shows all headings from H1 (`# Title`) to H3 (`### Title`) and is only displayed if there is more than one heading on the page. + +## Customization + +The table of contents is a functionality of the [[TableOfContents]] plugin. See the plugin page for more customization options. + +It also needs the `TableOfContents` component, which is displayed in the right sidebar by default. You can change this by customizing the [[layout]]. The TOC component can be configured with the `layout` parameter, which can either be `modern` (default) or `legacy`. diff --git a/Local/storage/thlab-notes/worker/docs/features/wikilinks.md b/Local/storage/thlab-notes/worker/docs/features/wikilinks.md new file mode 100644 index 0000000..bc1ce6b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/features/wikilinks.md @@ -0,0 +1,26 @@ +--- +title: Wikilinks +--- + +Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time. + +Quartz supports Wikilinks by default and these links are resolved by Quartz using the [[CrawlLinks]] plugin. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax. + +This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. + +Wikilink matching is case-insensitive to mirror Obsidian: `[[My Note]]`, `[[my note]]`, and `[[MY NOTE]]` all resolve to the same file. The generated URL is lowercased (e.g. `my-note`). + +## Syntax + +- `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file` +- `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override` +- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md` +- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md` + +### Embeds + +- `![[Path to image]]`: embeds an image into the page +- `![[Path to image|100x145]]`: embeds an image into the page with dimensions 100px by 145px +- `![[Path to file]]`: transclude an entire page +- `![[Path to file#Anchor]]`: transclude everything under the header `Anchor` +- `![[Path to file#^b15695]]`: transclude block with ID `^b15695` diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/authoring-content.md b/Local/storage/thlab-notes/worker/docs/getting-started/authoring-content.md new file mode 100644 index 0000000..088d025 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/authoring-content.md @@ -0,0 +1,49 @@ +--- +title: Authoring Content +aliases: + - "authoring content" +--- + +All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've followed the [[installation|installation guide]], this folder should already be initialized. Any Markdown in this folder will get processed by Quartz. + +It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments. + +Got everything set up? Preview your site locally with `npx quartz build --serve`, or see the [[build|build reference]] for more options. + +## Syntax + +As Quartz uses Markdown files as the main way of writing content, it fully supports Markdown syntax. By default, Quartz also ships with a few syntax extensions like [Github Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) (footnotes, strikethrough, tables, tasklists) and [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown) ([[callouts]], [[wikilinks]]). + +Additionally, Quartz also allows you to specify additional metadata in your notes called **frontmatter**. + +```md title="content/note.md" +--- +title: Example Title +draft: false +tags: + - example-tag +--- + +The rest of your content lives here. You can use **Markdown** here :) +``` + +Some common frontmatter fields that are natively supported by Quartz: + +- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title. +- `description`: Description of the page used for link previews. +- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes. +- `aliases`: Other names for this note. This is a list of strings. +- `tags`: Tags for this note. +- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz. +- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format. + +See [[Frontmatter]] for a complete list of frontmatter. + +## Syncing your Content + +When your Quartz is at a point you're happy with, you can save your changes to GitHub. +First, make sure you've [[installation#Setting Up Your GitHub Repository|set up your GitHub repository]] and then run `npx quartz sync`. + +## Customization + +Frontmatter parsing for `title`, `tags`, `aliases` and `cssclasses` is a functionality of the [[Frontmatter]] plugin, `date` is handled by the [[CreatedModifiedDate]] plugin and `description` by the [[Description]] plugin. See the plugin pages for customization options. diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/index.md b/Local/storage/thlab-notes/worker/docs/getting-started/index.md new file mode 100644 index 0000000..37e5eb6 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/index.md @@ -0,0 +1,34 @@ +--- +title: Getting Started +--- + +This guide walks you through setting up Quartz from scratch. If you already ran the [[index#🪴 Get Started|quickstart]] on the home page, you can skip ahead to whichever step you're on. + +## Prerequisites + +You need these tools installed before continuing: + +- **[Node.js](https://nodejs.org/) v22 or later** (run `node -v` to check) +- **npm v10.9.2 or later** (bundled with Node — run `npm -v` to check) +- **[Git](https://git-scm.com/)** (run `git -v` to check) + +> [!warning] Common issues +> +> - **Linux**: System packages (`apt install nodejs`) often ship much older versions. Use [nvm](https://github.com/nvm-sh/nvm) or the [NodeSource](https://github.com/nodesource/distributions) repository to get Node.js v22. +> - **Windows**: When installing Git, make sure **"Git from the command line and also from 3rd-party software"** is selected so that `git` is available in your terminal. If `node -v` or `git -v` shows "command not found", restart your terminal or check your PATH. +> - **macOS**: The Xcode command-line tools include Git (`xcode-select --install`). For Node.js, [nvm](https://github.com/nvm-sh/nvm) or the [official installer](https://nodejs.org/) both work. + +## Setup Steps + +Follow these in order: + +1. **[[installation|Installation]]** — Get Quartz (via GitHub template or clone), install dependencies, run the setup wizard (`npx quartz create`), install plugins, and preview your site locally +2. **[[authoring-content|Authoring Content]]** — Write and organize your Markdown notes in the `content/` folder +3. **[[installation#Setting Up Your GitHub Repository|Push to GitHub]]** — Create a repository and push your site with `npx quartz sync` +4. **[[hosting|Deploy]]** — Host your site for free on GitHub Pages, Cloudflare, Netlify, or Vercel + +## Upgrading & Migrating + +- **[[whats-new|What's New in Quartz 5]]** — Overview of new features and changes +- **[[upgrading|Upgrading Quartz]]** — Keep your Quartz installation up to date +- **[[migrating|Migrating to Quartz 5]]** — Migrate from Quartz 4 or Quartz 3 diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/installation.md b/Local/storage/thlab-notes/worker/docs/getting-started/installation.md new file mode 100644 index 0000000..b07ba45 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/installation.md @@ -0,0 +1,161 @@ +--- +title: "Installation" +aliases: + - "setting up your GitHub repository" +--- + +This page walks you through the full Quartz setup: from getting the source code to previewing your site locally, then pushing it to GitHub. + +## 1. Get Quartz + +There are two ways to get started. Pick whichever you prefer: + +### Option A: Use the GitHub Template (Recommended) + +> [!tip] Why this option? +> Using the template creates your own repository in one click — no need to reconfigure Git remotes later. + +1. Go to the [Quartz repository](https://github.com/jackyzha0/quartz) and click **Use this template** → **Create a new repository** +2. Give your repository a name (e.g. `quartz`, `notes`, `garden`), choose public or private, then click **Create repository** +3. Clone **your new repository** and enter the folder: + +```bash +git clone https://github.com//.git +cd +``` + +### Option B: Clone Directly + +If you don't use GitHub or prefer a manual setup: + +```bash +git clone https://github.com/jackyzha0/quartz.git +cd quartz +``` + +> [!note] +> With this option, you'll need to [[#Connect Your Local Clone|point the `origin` remote]] to your own repository later when you're ready to publish. + +## 2. Install Dependencies + +> [!important] +> Quartz requires **Node.js 22** or later. Check your version with `node -v` and upgrade at [nodejs.org](https://nodejs.org/) if needed. + +```bash +npm i +``` + +> [!note] +> On subsequent clones of your own repository (e.g. on a new machine), use `npm ci` instead for a faster, reproducible install from the lockfile. + +## 3. Initialize Your Site + +Run the interactive setup wizard: + +```bash +npx quartz create +``` + +This will prompt you for: + +- A **template** (`default`, `obsidian`, `ttrpg`, `blog`) — pick the one that matches your use case. See [[create#Templates]] for details on each. +- A **content strategy** — choose how to populate the `content/` folder: + - **new**: Start with an empty folder + - **copy**: Copy files from an existing folder (e.g. your Obsidian vault) + - **symlink**: Link to an existing folder so changes sync automatically +- A **base URL** — the URL where your site will be deployed (e.g. `mysite.github.io/quartz`). Don't include `https://`. +- A **link resolution** strategy — how to resolve internal links (`shortest`, `absolute`, or `relative`). Skipped for Obsidian and TTRPG templates. + +For non-interactive usage and more details, see the [[create|`quartz create` CLI reference]]. + +## 4. Install Plugins + +The template you chose references community plugins that need to be installed: + +```bash +npx quartz plugin install --from-config +``` + +This downloads and builds all plugins listed in `quartz.config.yaml` into `.quartz/plugins/`. + +> [!tip] +> If some plugins fail to build, try refreshing them to their latest versions: +> +> ```bash +> npx quartz plugin install --latest +> ``` +> +> See [[troubleshooting#Plugins fail to build on a fresh clone]] for more details. + +## 5. Preview Your Site + +```bash +npx quartz build --serve +``` + +Your site is now running at `http://localhost:8080`. The dev server watches for file changes and reloads automatically. + +At this point you can [[authoring-content|start writing content]] in the `content/` folder. When you're ready to publish, continue below to push your site to GitHub and [[hosting|deploy it]]. + +--- + +## Setting Up Your GitHub Repository + +> [!note] +> If you used **Option A** (GitHub Template) in step 1, your repository already exists and `origin` is already set. You can skip straight to [[#Push Your Site]]. + +To publish your site, you'll need your own GitHub repository. This section is for **Option B** (direct clone) users. + +### Create the Repository + +Create a new repository on [GitHub.com](https://github.com/new). Do **not** initialize it with a README, license, or `.gitignore` — Quartz already includes these files, and duplicating them will cause merge conflicts on your first push. + +![[github-init-repo-options.png]] + +Copy the repository URL from the Quick Setup page: + +![[github-quick-setup.png]] + +### Connect Your Local Clone + +Point your local Quartz at your new repository: + +```bash +# Check current remotes +git remote -v + +# Point origin to your repository +git remote set-url origin REMOTE-URL +``` + +> [!tip] +> You don't need to add an `upstream` remote manually — `npx quartz create` already configured it for you. The upstream remote is used by `npx quartz upgrade` to pull in future Quartz updates. + +### Push Your Site + +```bash +npx quartz sync --no-pull +``` + +This commits your content and pushes everything to your repository. For subsequent updates, just run: + +```bash +npx quartz sync +``` + +> [!hint] Flags and options +> For full help options, you can run `npx quartz sync --help`. +> +> Most of these have sensible defaults but you can override them if you have a custom setup: +> +> - `-d` or `--directory`: the content folder. This is normally just `content` +> - `-v` or `--verbose`: print out extra logging information +> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes +> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz +> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing + +## Next Steps + +- **[[authoring-content|Authoring Content]]** — Write and organize your notes +- **[[hosting|Hosting]]** — Deploy your site to GitHub Pages, Cloudflare, Netlify, or Vercel +- **[[configuration|Configuration]]** — Customize your site's appearance and behavior diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/migrating.md b/Local/storage/thlab-notes/worker/docs/getting-started/migrating.md new file mode 100644 index 0000000..6e752ce --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/migrating.md @@ -0,0 +1,228 @@ +--- +title: "Migrating to Quartz 5" +aliases: + - "migrating from Quartz 3" + - "migrating from Quartz 4" +--- + +This guide covers migrating to Quartz 5 from previous versions. If you're already on Quartz 5 and want to update to the latest version, see [[upgrading|Upgrading Quartz]] instead. + +If you're new to Quartz entirely, skip this guide and follow the [[installation|installation guide]] instead. + +## Before You Start: Save Your Content + +Before switching branches, make sure your content is safe. Switching to v5 will replace the files in your working directory with the v5 codebase, so your v4 content folder won't be visible until you restore it. + +Copy your content folder somewhere outside the repo before switching: + +```bash +# macOS / Linux +cp -r content /tmp/quartz-content + +# Windows (PowerShell) +Copy-Item -Recurse content $env:TEMP\quartz-content +``` + +> [!note] Your old branch is preserved +> Switching branches does **not** delete your v4 (or v3/hugo) branch. You can always switch back with `git checkout v4` to access your old content and configuration. + +## Getting the v5 Branch + +Whether you're coming from Quartz 4 or Quartz 3, the first step is the same: get the v5 branch onto your machine and push it to your repository. + +```bash +# Add the official Quartz repository as a remote called "upstream" (skip if already set) +git remote add upstream https://github.com/jackyzha0/quartz.git + +# Fetch the v5 branch from the official repository +git fetch upstream v5 + +# Create a local v5 branch from the official one +git checkout -b v5 upstream/v5 + +# Install dependencies +npm i + +# Push v5 to your GitHub repository +git push -u origin v5 +``` + +## Setting Up Your Site + +Once you're on v5, run the interactive setup to configure your site and import your content: + +```bash +npx quartz create +``` + +This will prompt you for: + +- A **template** (`default`, `obsidian`, `ttrpg`, `blog`) — pick the one closest to your old setup. `obsidian` is recommended if you use an Obsidian vault. +- A **content strategy** — choose "Copy" and point it to your backed-up content folder. + +If you skipped the `create` wizard or need to restore your content manually: + +```bash +# macOS / Linux +cp -r /tmp/quartz-content/* content/ + +# Windows (PowerShell) +Copy-Item -Recurse $env:TEMP\quartz-content\* content\ +``` + +After running `create`, install all plugins referenced in the generated config: + +```bash +npx quartz plugin install --from-config +``` + +## What Changed in v5 + +Quartz 5 introduces a community plugin system that fundamentally changes how plugins and components are managed. Most plugins that were built into Quartz 4 are now standalone community plugins maintained under the [quartz-community](https://github.com/quartz-community) organization. + +Key changes: + +- **Configuration format**: TypeScript (`quartz.config.ts`, `quartz.layout.ts`) → YAML (`quartz.config.yaml`) +- **Plugin system**: Plugins are now standalone Git repositories, installed via `npx quartz plugin add` +- **Import pattern**: Community plugins use `ExternalPlugin.X()` (from `.quartz/plugins`) instead of `Plugin.X()` (from `./quartz/plugins`) +- **Layout structure**: `quartz.layout.ts` is gone — layout position is now a per-plugin property in `quartz.config.yaml` +- **Page types**: A new plugin category for page rendering (content, folder, tag pages) +- **URL casing**: All generated URLs are now lowercased and hyphenated (e.g. `My Notes/Hello World.md` → `/my-notes/hello-world`). In v4, the original casing of file and folder names was preserved in URLs. + +### URL Casing and SEO + +If your v4 site had URLs with uppercase letters, those URLs will return 404 errors after upgrading to v5. This also affects search engine indexing, since Google treats URLs as [case-sensitive](https://developers.google.com/search/docs/crawling-indexing/url-structure). + +The [[AliasRedirects]] plugin (enabled by default) automatically handles this. During build, it detects files whose original path contained uppercase characters and generates redirect pages at the old URLs. These redirect pages include proper SEO signals (``, ``, ``) so that search engines transfer ranking to the new lowercase URLs. + +No manual configuration is needed — the plugin is enabled by default and the case redirect behavior is on by default. If you want to disable it, set `enableCaseRedirects: false` in the plugin options: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/alias-redirects + enabled: true + options: + enableCaseRedirects: false +``` + +> [!tip] Hosting on Netlify? +> Netlify automatically lowercases all URLs and issues server-side 301 redirects. If you're hosting on Netlify, the case redirect pages aren't strictly necessary, but they don't hurt either. + +> [!note] Most users don't need to worry about these details +> If you used the default Quartz 4 configuration (or only changed settings that `npx quartz create` prompts for), the setup wizard handles everything. The details below are for users who had custom plugin configurations. + +### Plugin Reference Table + +Mapping v4 plugin names to v5 equivalents: + +| v4 | v5 | Type | +| ----------------------------------- | ------------------------------------------- | --------------------- | +| `Plugin.FrontMatter()` | `ExternalPlugin.NoteProperties()` | Community | +| `Plugin.CreatedModifiedDate()` | `ExternalPlugin.CreatedModifiedDate()` | Community | +| `Plugin.SyntaxHighlighting()` | `ExternalPlugin.SyntaxHighlighting()` | Community | +| `Plugin.ObsidianFlavoredMarkdown()` | `ExternalPlugin.ObsidianFlavoredMarkdown()` | Community | +| `Plugin.GitHubFlavoredMarkdown()` | `ExternalPlugin.GitHubFlavoredMarkdown()` | Community | +| `Plugin.CrawlLinks()` | `ExternalPlugin.CrawlLinks()` | Community | +| `Plugin.Description()` | `ExternalPlugin.Description()` | Community | +| `Plugin.Latex()` | `ExternalPlugin.Latex()` | Community | +| `Plugin.RemoveDrafts()` | `ExternalPlugin.RemoveDrafts()` | Community | +| `Plugin.ContentPage()` | `ExternalPlugin.ContentPage()` | Community (pageTypes) | +| `Plugin.FolderPage()` | `ExternalPlugin.FolderPage()` | Community (pageTypes) | +| `Plugin.TagPage()` | `ExternalPlugin.TagPage()` | Community (pageTypes) | +| `Plugin.NotFoundPage()` | `Plugin.PageTypes.NotFoundPageType()` | Internal (pageTypes) | +| `Plugin.ComponentResources()` | `Plugin.ComponentResources()` (unchanged) | Internal | +| `Plugin.Assets()` | `Plugin.Assets()` (unchanged) | Internal | +| `Plugin.Static()` | `Plugin.Static()` (unchanged) | Internal | +| `Plugin.AliasRedirects()` | `ExternalPlugin.AliasRedirects()` | Community | +| `Plugin.ContentIndex()` | `ExternalPlugin.ContentIndex()` | Community | + +Component layout mapping: + +| v4 Layout | v5 Layout | +| ----------------------------- | ---------------------------------------- | +| `Component.Explorer()` | `Plugin.Explorer()` | +| `Component.Graph()` | `Plugin.Graph()` | +| `Component.Search()` | `Plugin.Search()` | +| `Component.Backlinks()` | `Plugin.Backlinks()` | +| `Component.Darkmode()` | `Plugin.Darkmode()` | +| `Component.Footer()` | `Plugin.Footer()` | +| `Component.TableOfContents()` | `Plugin.TableOfContents()` | +| `Component.Head()` | `Component.Head()` (unchanged, internal) | +| `Component.Spacer()` | `Plugin.Spacer()` | + +## Updating Your CI/CD + +Quartz 5 requires plugins to be installed before building. Add a plugin install step and (optionally) caching to your CI pipeline. + +Here's the recommended pattern, based on the project's own GitHub Actions: + +```yaml +- name: Cache dependencies + uses: actions/cache@v5 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + +- name: Cache Quartz plugins + uses: actions/cache@v5 + with: + path: .quartz/plugins + key: ${{ runner.os }}-plugins-${{ hashFiles('quartz.lock.json') }} + restore-keys: | + ${{ runner.os }}-plugins- + +- run: npm ci + +- name: Install Quartz plugins + run: npx quartz plugin install + +- name: Build Quartz + run: npx quartz build +``` + +The plugin cache uses `quartz.lock.json` as the cache key, so plugins are only re-downloaded when the lockfile changes. + +For non-GitHub CI providers (Cloudflare, Vercel, Netlify), the build command should be: + +```shell +npx quartz plugin install && npx quartz build +``` + +See [[hosting]] for provider-specific setup details. + +## Setting Your Default Branch to v5 + +After verifying your site builds and deploys correctly, update your repository's default branch to `v5`: + +1. Go to your repository on GitHub +2. Navigate to **Settings** → **General** +3. Under **Default branch**, click the switch icon next to your current default branch +4. Select `v5` from the dropdown and click **Update** +5. Confirm the change + +This ensures that new clones, pull requests, and GitHub Pages deployments all target v5 by default. Your old v4 branch remains available for reference. + +> [!warning] Update your CI triggers +> If your CI workflow triggers on a specific branch (e.g. `branches: [v4]`), make sure to update it to `v5`. See the [[hosting]] guide for examples. + +## Notes for Quartz 3 Users + +If you're coming from Quartz 3 (the Hugo-based version), follow the same steps above — get the v5 branch, run `npx quartz create`, and import your content. There is no need to go through Quartz 4 first. + +### Key changes from Quartz 3 + +1. **Hugo is gone**: Quartz now uses a Node-based static-site generation process. No more Go templates or `hugo-obsidian`. +2. **Full hot-reload**: The development server (`npx quartz build --serve`) re-processes all content on every change. +3. **JSX instead of Go templates**: Layout components are written in JSX (JavaScript XML), which is significantly easier to customize. +4. **New plugin system**: See [[configuration#Plugins|Plugins]] for details on the extensible plugin architecture. + +### Things to update + +- Update your deploy scripts — see the [[hosting]] guide. +- Ensure your default branch on GitHub is updated to `v5`. +- [[folder and tag listings|Folder and tag listings]] have changed: + - Folder descriptions go under `content//index.md` + - Tag descriptions go under `content/tags/.md` +- Custom CSS may need updates if you depended on specific HTML hierarchy or class names from Quartz 3. diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/upgrading.md b/Local/storage/thlab-notes/worker/docs/getting-started/upgrading.md new file mode 100644 index 0000000..00bf5eb --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/upgrading.md @@ -0,0 +1,40 @@ +--- +title: "Upgrading Quartz" +aliases: + - upgrading +--- + +> [!note] +> This is specifically a guide for upgrading your Quartz to a more recent update. If you are coming from Quartz 4 or Quartz 3, check out the [[migrating|migration guide]] for more info. + +To fetch the latest Quartz updates, simply run + +```bash +npx quartz upgrade +``` + +As Quartz uses [git](https://git-scm.com/) under the hood for versioning, upgrading effectively 'pulls' in the updates from the official Quartz GitHub repository. Merge conflicts in `quartz.lock.json` are handled automatically — Quartz backs up your lockfile before pulling and restores it afterward. For other files with local changes that conflict with the updates, you may need to resolve these manually yourself (or, pull manually using `git pull origin upstream`). + +> [!hint] +> Quartz will try to cache your content before upgrading to try and prevent merge conflicts. If you get a conflict mid-merge, you can stop the merge and then run `npx quartz restore` to restore your content from the cache. + +If you have the [GitHub desktop app](https://desktop.github.com/), this will automatically open to help you resolve the conflicts. Otherwise, you will need to resolve this in a text editor like VSCode. For more help on resolving conflicts manually, check out the [GitHub guide on resolving merge conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line#competing-line-change-merge-conflicts). + +To update your installed plugins separately, use: + +```bash +npx quartz plugin install --latest +``` + +See the [[upgrade|CLI reference for upgrade]] for more details on available flags. + +### Cleaning Up Unused Plugins + +If you've removed plugins from your configuration during an upgrade, you can clean up the leftover files: + +```bash +npx quartz plugin prune --dry-run # preview what would be removed +npx quartz plugin prune # remove orphaned plugins +``` + +See the [[cli/plugin#prune|plugin prune reference]] for more details. diff --git a/Local/storage/thlab-notes/worker/docs/getting-started/whats-new.md b/Local/storage/thlab-notes/worker/docs/getting-started/whats-new.md new file mode 100644 index 0000000..129909f --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/getting-started/whats-new.md @@ -0,0 +1,193 @@ +--- +title: "What's New in Quartz 5" +aliases: + - "changelog" + - "v5" +--- + +Quartz 5 is a ground-up rearchitecture of Quartz focused on extensibility, performance, and Obsidian compatibility. If you're coming from v4, see [[migrating|Migrating to Quartz 5]] for the upgrade path. + +## Plugin Ecosystem + +The biggest change in v5 is the move to a **community plugin ecosystem**. Plugins are now standalone packages maintained in the [quartz-community](https://github.com/quartz-community) GitHub organization and installed via git: + +```bash +npx quartz plugin add github:quartz-community/explorer +``` + +This means: + +- **Independent versioning**: Plugins can be updated without upgrading Quartz itself +- **Community contributions**: Anyone can publish a Quartz plugin +- **Smaller core**: Quartz core is leaner; features live in plugins +- **Plugin registry**: Discover plugins via `npx quartz tui` or the [plugin registry](https://github.com/quartz-community/registry) + +Over 40 official plugins ship with Quartz, covering everything from search and graph view to encrypted pages and canvas rendering. + +## YAML Configuration + +Configuration moved from TypeScript (`quartz.config.ts`) to **YAML** (`quartz.config.yaml`): + +```yaml title="quartz.config.yaml" +configuration: + pageTitle: My Digital Garden + enableSPA: true + enablePopovers: true + locale: en-US + baseUrl: mysite.github.io + theme: + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono +plugins: + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + order: 30 + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 +``` + +Benefits: + +- **No TypeScript knowledge required** for basic customization +- **JSON Schema validation** — editors with YAML support show errors inline +- **Layout defined per-plugin** — each plugin declares its own position and priority +- **Templates** — `npx quartz create` offers preconfigured templates (default, obsidian, ttrpg, blog) + +For advanced options that need JavaScript (callbacks, custom components), the `quartz.ts` override system provides full programmatic control. + +## Improved Obsidian Compatibility + +Quartz 5 aims for full compatibility with Obsidian's core features: + +- **Wikilinks** — all variations including aliases, headings, block references, and pipe escaping in tables +- **Callouts** — all built-in types, collapsible variants, and nested callouts +- **Highlights** — `==highlighted text==` syntax +- **Comments** — `%%hidden comments%%` (inline and block) +- **Tags** — `#tag` and `#nested/tag` with tag pages +- **Custom task characters** — `[?]`, `[!]`, `[>]`, etc. preserved as `data-task` attributes +- **Mermaid diagrams** — rendered with expand button +- **YouTube and Tweet embeds** — via image syntax +- **Block references** — `^block-id` with broad character support +- **Video/audio embeds** — full format support (mp4, webm, ogv, mov, mkv, avi, flac, aac, etc.) +- **Canvas files** — rendered as interactive, pannable pages via the canvas-page plugin +- **Obsidian URI links** — marked with CSS class for custom styling +- **Footnotes** — via the GitHub Flavored Markdown plugin + +See [[Obsidian compatibility]] for the full list. + +## Page Type System + +Quartz 5 introduces **page types** — plugins that define how different kinds of pages are rendered: + +- **Content pages** — regular markdown notes +- **Folder pages** — directory listing pages +- **Tag pages** — pages listing notes with a given tag +- **Canvas pages** — interactive JSON Canvas renderings +- **Bases pages** — database-style views of your content + +Each page type can use a different [[layout#Page Frames|page frame]] for fundamentally different HTML structures (three-column, full-width, minimal, etc.). + +## Layout System + +The layout system is now declarative. Plugins declare their position (`left`, `right`, `beforeBody`, `afterBody`) and priority in the config: + +```yaml +plugins: + - source: github:quartz-community/explorer + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + layout: + position: right + priority: 10 +``` + +Additional features: + +- **Groups** — combine components into flex rows/columns (e.g., toolbar with search + darkmode toggle) +- **Conditional rendering** — show/hide components based on page properties (`condition: not-index`, `condition: has-tags`) +- **Display modifiers** — `display: mobile-only` or `display: desktop-only` +- **Per-page-type overrides** — different layouts for content, folder, tag, and 404 pages + +## Performance + +- **Parallel processing** — markdown parsing uses a worker pool across all CPU cores +- **Incremental rebuilds** — watch mode only re-processes changed files +- **Pre-built plugins** — community plugins ship compiled `dist/` directories, skipping build-from-source on install +- **SPA routing** — client-side navigation with `micromorph` for instant page transitions +- **CDN-cached fonts** — Google Fonts with aggressive caching, or fully self-hosted with `fontOrigin: local` + +## CLI Improvements + +The CLI is simpler and more helpful: + +| Command | Description | +| -------------------------------- | --------------------------------------- | +| `npx quartz create` | Interactive setup wizard with templates | +| `npx quartz build --serve` | Build and serve with hot reload | +| `npx quartz sync` | Commit and push to GitHub | +| `npx quartz upgrade` | Pull latest Quartz updates | +| `npx quartz plugin install` | Install plugins from lockfile | +| `npx quartz plugin add ` | Add a new plugin | +| `npx quartz plugin list` | List installed plugins | +| `npx quartz plugin prune` | Remove unused plugins | + +Other improvements: + +- **Node.js version check** — clear error message if running on Node < 22 +- **Port conflict handling** — helpful message when port is already in use +- **Plugin lockfile** — `quartz.lock.json` pins plugin versions for reproducible builds +- **Concurrency control** — `--concurrency` flag for memory-constrained environments + +## Internationalization + +Quartz 5 supports multiple locales out of the box. Set `locale: ja-JP` (or any supported locale) in your config to translate all UI strings — search placeholders, "table of contents", date formatting, and more. + +## New Plugins + +Plugins new to v5 (not available in v4): + +```base +filters: + and: + - file.ext == "md" + - file.inFolder("plugins") + - note["new-in-v5"] == true +properties: + title: + displayName: Plugin + repository: + displayName: Repository + description: + displayName: Description +views: + - type: table + name: New in v5 + order: + - title + - repository + - description + sort: + - property: title + direction: ASC +``` + +## For Plugin Developers + +If you built plugins for v4, the development model has changed significantly: + +- Plugins are **standalone npm packages** with their own `package.json`, `tsconfig.json`, and build system +- The **factory function pattern** (inspired by Astro integrations) replaces class-based plugins +- **`@quartz-community/types`** provides full type safety without depending on the Quartz core +- **`@quartz-community/utils`** provides shared path, DOM, and language utilities +- **`@quartz-community/runtime`** provides browser runtime utilities +- Plugins can ship **components**, **frames**, **stylesheets**, and **client scripts** +- A **plugin template** is available at [quartz-community/plugin-template](https://github.com/quartz-community/plugin-template) + +See [[making plugins]] for the full guide. diff --git a/Local/storage/thlab-notes/worker/docs/hosting.md b/Local/storage/thlab-notes/worker/docs/hosting.md new file mode 100644 index 0000000..afae47f --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/hosting.md @@ -0,0 +1,347 @@ +--- +title: Hosting +--- + +Quartz effectively turns your Markdown files and other resources into a bundle of HTML, JS, and CSS files (a website!). + +However, if you'd like to publish your site to the world, you need a way to host it online. This guide will detail how to deploy with common hosting providers but any service that allows you to deploy static HTML should work as well. + +> [!warning] +> The rest of this guide assumes that you've already created your own GitHub repository for Quartz. If you haven't already, follow the [[installation#Setting Up Your GitHub Repository|GitHub repository setup]] section of the installation guide. + +> [!hint] +> Some Quartz features (like [[RSS Feed]] and sitemap generation) require `baseUrl` to be configured properly in your [[configuration]] to work properly. Make sure you set this before deploying! + +> [!tip] Keeping plugins in sync +> All hosting examples below use `npx quartz plugin install` to install plugins from the lockfile. If contributors may add plugins to `quartz.config.yaml` without updating the lockfile, add `npx quartz plugin install --from-config` after `install` in your build command to install any missing plugins. See [[cli/plugin#install|plugin install]] for details. + +## Cloudflare Pages + +1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select your account. +2. In Account Home, select **Compute (Workers)** > **Workers & Pages** > **Create application** > **Pages** > **Connect to Git**. +3. Select the new GitHub repository that you created and, in the **Set up builds and deployments** section, provide the following information: + +| Configuration option | Value | +| ---------------------- | ----------------------------------------------- | +| Production branch | `v5` | +| Framework preset | `None` | +| Build command | `npx quartz plugin install && npx quartz build` | +| Build output directory | `public` | + +Press "Save and deploy" and Cloudflare should have a deployed version of your site in about a minute. Then, every time you sync your Quartz changes to GitHub, your site should be updated. + +To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/). + +> [!warning] +> Cloudflare Pages performs a shallow clone by default, so if you rely on `git` for timestamps, it is recommended that you add `git fetch --unshallow &&` to the beginning of the build command (e.g., `git fetch --unshallow && npx quartz plugin install && npx quartz build`). + +> [!note] +> For more detailed CI/CD configuration including caching and plugin management, see [[migrating#Updating Your CI/CD|the migration guide]]. + +## GitHub Pages + +In your local Quartz, create a new file `quartz/.github/workflows/deploy.yml`. + +```yaml title="quartz/.github/workflows/deploy.yml" +name: Deploy Quartz site to GitHub Pages + +on: + push: + branches: + - v5 + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Fetch all history for git info + - uses: actions/setup-node@v6 + with: + node-version: 24 + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Cache Quartz plugins + uses: actions/cache@v5 + with: + path: .quartz/plugins + key: ${{ runner.os }}-plugins-${{ hashFiles('quartz.lock.json') }} + restore-keys: | + ${{ runner.os }}-plugins- + - name: Install Dependencies + run: npm ci + - name: Install Quartz plugins + run: npx quartz plugin install + - name: Build Quartz + run: npx quartz build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: public + + deploy: + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 +``` + +Then: + +1. Head to "Settings" tab of your forked repository and in the sidebar, click "Pages". Under "Source", select "GitHub Actions". +2. Commit these changes by doing `npx quartz sync`. This should deploy your site to `.github.io/`. + +> [!hint] +> If you get an error about not being allowed to deploy to `github-pages` due to environment protection rules, make sure you remove any existing GitHub pages environments. +> +> You can do this by going to your Settings page on your GitHub fork and going to the Environments tab and pressing the trash icon. The GitHub action will recreate the environment for you correctly the next time you sync your Quartz. + +> [!info] +> Quartz generates files in the format of `file.html` instead of `file/index.html` which means the trailing slashes for _non-folder paths_ are dropped. As GitHub pages does not do this redirect, this may cause existing links to your site that use trailing slashes to break. If not breaking existing links is important to you (e.g. you are migrating from Quartz 3), consider using [[#Cloudflare Pages]]. + +### Custom Domain + +Here's how to add a custom domain to your GitHub pages deployment. + +1. Head to the "Settings" tab of your forked repository. +2. In the "Code and automation" section of the sidebar, click "Pages". +3. Under "Custom Domain", type your custom domain and click "Save". +4. This next step depends on whether you are using an apex domain (`example.com`) or a subdomain (`subdomain.example.com`). + - If you are using an apex domain, navigate to your DNS provider and create an `A` record that points your apex domain to GitHub's name servers which have the following IP addresses: + - `185.199.108.153` + - `185.199.109.153` + - `185.199.110.153` + - `185.199.111.153` + - If you are using a subdomain, navigate to your DNS provider and create a `CNAME` record that points your subdomain to the default domain for your site. For example, if you want to use the subdomain `quartz.example.com` for your user site, create a `CNAME` record that points `quartz.example.com` to `.github.io`. + +![[dns records.png]]_The above shows a screenshot of Google Domains configured for both `jzhao.xyz` (an apex domain) and `quartz.jzhao.xyz` (a subdomain)._ + +See the [GitHub documentation](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-a-subdomain) for more detail about how to setup your own custom domain with GitHub Pages. + +> [!question] Why aren't my changes showing up? +> There could be many different reasons why your changes aren't showing up but the most likely reason is that you forgot to push your changes to GitHub. +> +> Make sure you save your changes to Git and sync it to GitHub by doing `npx quartz sync`. This will also make sure to pull any updates you may have made from other devices so you have them locally. + +## Vercel + +### Fix URLs + +Before deploying to Vercel, a `vercel.json` file is required at the root of the project directory. It needs to contain the following configuration so that URLs don't require the `.html` extension: + +```json title="vercel.json" +{ + "cleanUrls": true +} +``` + +### Deploy to Vercel + +1. Log in to the [Vercel Dashboard](https://vercel.com/dashboard) and click "Add New..." > Project +2. Import the Git repository containing your Quartz project. +3. Give the project a name (lowercase characters and hyphens only) +4. Check that these configuration options are set: + +| Configuration option | Value | +| ----------------------------------------- | ----------------------------------------------- | +| Framework Preset | `Other` | +| Root Directory | `./` | +| Build and Output Settings > Build Command | `npx quartz plugin install && npx quartz build` | + +5. Press Deploy. Once it's live, you'll have 2 `*.vercel.app` URLs to view the page. + +### Custom Domain + +> [!note] +> If there is something already hosted on the domain, these steps will not work without replacing the previous content. As a workaround, you could use Next.js rewrites or use the next section to create a subdomain. + +1. Update the `baseUrl` in `quartz.config.yaml` if necessary. +2. Go to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel. +3. Connect the domain to Vercel +4. Press "Add" to connect a custom domain to Vercel. +5. Select your Quartz repository and press Continue. +6. Enter the domain you want to connect it to. +7. Follow the instructions to update your DNS records until you see "Valid Configuration" + +### Use a Subdomain + +Using `docs.example.com` is an example of a subdomain. They're a simple way of connecting multiple deployments to one domain. + +1. Update the `baseUrl` in `quartz.config.yaml` if necessary. +2. Ensure your domain has been added to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel. +3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project. +4. Go to the Settings tab and then click Domains in the sidebar +5. Enter your subdomain into the field and press Add + +## Netlify + +1. Log in to the [Netlify dashboard](https://app.netlify.com/) and click "Add new site". +2. Select your Git provider and repository containing your Quartz project. +3. Under "Build command", enter `npx quartz plugin install && npx quartz build`. +4. Under "Publish directory", enter `public`. +5. Press Deploy. Once it's live, you'll have a `*.netlify.app` URL to view the page. +6. To add a custom domain, check "Domain management" in the left sidebar, just like with Vercel. + +## GitLab Pages + +In your local Quartz, create a new file `.gitlab-ci.yml`. + +```yaml title=".gitlab-ci.yml" +stages: + - build + - deploy + +image: node:24 +cache: + - key: npm-$CI_COMMIT_REF_SLUG + paths: + - .npm/ + - key: plugins-$CI_COMMIT_REF_SLUG + paths: + - .quartz/plugins/ + +build: + stage: build + rules: + - if: '$CI_COMMIT_REF_NAME == "v5"' + before_script: + - hash -r + - npm ci --cache .npm --prefer-offline + script: + - npx quartz plugin install + - npx quartz build + artifacts: + paths: + - public + +pages: + stage: deploy + rules: + - if: '$CI_COMMIT_REF_NAME == "v5"' + script: + - echo "Deploying to GitLab Pages..." + artifacts: + paths: + - public +``` + +When `.gitlab-ci.yaml` is committed, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar. + +By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`. + +## Self-Hosting + +Copy the `public` directory to your web server and configure it to serve the files. You can use any web server to host your site. Since Quartz generates links that do not include the `.html` extension, you need to let your web server know how to deal with it. + +### Using Nginx + +Here's an example of how to do this with Nginx: + +```nginx title="nginx.conf" +server { + listen 80; + server_name example.com; + root /path/to/quartz/public; + index index.html; + error_page 404 /404.html; + + location / { + try_files $uri $uri.html $uri/ =404; + } +} +``` + +### Using Apache + +Here's an example of how to do this with Apache: + +```apache title=".htaccess" +RewriteEngine On + +ErrorDocument 404 /404.html + +# Rewrite rule for .html extension removal (with directory check) +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI}.html -f +RewriteRule ^(.*)$ $1.html [L] + +# Handle directory requests explicitly +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^(.*)/$ $1/index.html [L] +``` + +Don't forget to activate brotli / gzip compression. + +### Using Caddy + +Here's and example of how to do this with Caddy: + +```caddy title="Caddyfile" +example.com { + root * /path/to/quartz/public + try_files {path} {path}.html {path}/ =404 + file_server + encode gzip + + handle_errors { + rewrite * /{err.status_code}.html + file_server + } +} +``` + +## Caching + +Quartz emits CSS and JS files with content hashes in their filenames (e.g. `index-a3f2c1b.css`, `component-7d4e2f.css`). Since the filename changes whenever the content changes, these files can be cached indefinitely. HTML files should not be cached long-term since they reference the hashed filenames and need to stay fresh. + +### Cloudflare Pages / Vercel / Netlify + +These platforms handle caching automatically. No configuration is needed — hashed assets will be served with appropriate cache headers out of the box. + +### Nginx + +```nginx title="nginx.conf" +# Immutable cache for hashed assets +location ~* \.(css|js)$ { + if ($uri ~* "-[0-9a-f]{8}\.") { + add_header Cache-Control "public, max-age=31536000, immutable"; + } +} +``` + +### Caddy + +```caddy title="Caddyfile" +@hashed path_regexp hashed -[0-9a-f]{8}\.(css|js)$ +header @hashed Cache-Control "public, max-age=31536000, immutable" +``` + +### Apache + +```apache title=".htaccess" +# Immutable cache for content-hashed assets + + Header set Cache-Control "public, max-age=31536000, immutable" + +``` diff --git a/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-dark.png b/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-dark.png new file mode 100644 index 0000000..60c4e85 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-dark.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-light.png b/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-light.png new file mode 100644 index 0000000..046a407 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/custom-social-image-preview-light.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/dns records.png b/Local/storage/thlab-notes/worker/docs/images/dns records.png new file mode 100644 index 0000000..bf9f854 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/dns records.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/giscus-discussion.png b/Local/storage/thlab-notes/worker/docs/images/giscus-discussion.png new file mode 100644 index 0000000..939af62 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/giscus-discussion.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/giscus-example.png b/Local/storage/thlab-notes/worker/docs/images/giscus-example.png new file mode 100644 index 0000000..f59f52b Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/giscus-example.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/giscus-repo.png b/Local/storage/thlab-notes/worker/docs/images/giscus-repo.png new file mode 100644 index 0000000..bfabc56 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/giscus-repo.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/giscus-results.png b/Local/storage/thlab-notes/worker/docs/images/giscus-results.png new file mode 100644 index 0000000..b25c751 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/giscus-results.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/github-init-repo-options.png b/Local/storage/thlab-notes/worker/docs/images/github-init-repo-options.png new file mode 100644 index 0000000..dd88931 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/github-init-repo-options.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/github-quick-setup.png b/Local/storage/thlab-notes/worker/docs/images/github-quick-setup.png new file mode 100644 index 0000000..5be333f Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/github-quick-setup.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/quartz transform pipeline.png b/Local/storage/thlab-notes/worker/docs/images/quartz transform pipeline.png new file mode 100644 index 0000000..657f0a3 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/quartz transform pipeline.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/quartz-layout-desktop.png b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-desktop.png new file mode 100644 index 0000000..461d791 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-desktop.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/quartz-layout-mobile.png b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-mobile.png new file mode 100644 index 0000000..ad6c09e Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-mobile.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/quartz-layout-tablet.png b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-tablet.png new file mode 100644 index 0000000..6349f29 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/quartz-layout-tablet.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/social-image-preview-dark.png b/Local/storage/thlab-notes/worker/docs/images/social-image-preview-dark.png new file mode 100644 index 0000000..c125451 Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/social-image-preview-dark.png differ diff --git a/Local/storage/thlab-notes/worker/docs/images/social-image-preview-light.png b/Local/storage/thlab-notes/worker/docs/images/social-image-preview-light.png new file mode 100644 index 0000000..ca0bdbc Binary files /dev/null and b/Local/storage/thlab-notes/worker/docs/images/social-image-preview-light.png differ diff --git a/Local/storage/thlab-notes/worker/docs/index.md b/Local/storage/thlab-notes/worker/docs/index.md new file mode 100644 index 0000000..5353074 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/index.md @@ -0,0 +1,69 @@ +--- +title: Welcome to Quartz 5 +--- + +Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, websites, and [digital gardens](https://jzhao.xyz/posts/networked-thought) to the web. + +## 🪴 Get Started + +Quartz requires **at least [Node](https://nodejs.org/) v22** and `npm` v10.9.2 to function correctly. Ensure you have these installed on your machine before continuing. See the [[getting-started/index#Prerequisites|prerequisites]] for help installing them. + +> [!tip] GitHub users +> You can also use the **[GitHub template](https://github.com/jackyzha0/quartz/generate)** to create your repository in one click, then clone that instead. See [[installation#Option A Use the GitHub Template Recommended|Option A]] in the installation guide. + +```shell +# 1. Clone the Quartz repository +git clone https://github.com/jackyzha0/quartz.git +cd quartz + +# 2. Install dependencies +npm i + +# 3. Initialize your site (choose a template, set your base URL, import content) +npx quartz create + +# 4. Install plugins referenced by your chosen template +npx quartz plugin install --from-config + +# 5. Preview your site locally +npx quartz build --serve +``` + +Your site is now running at `http://localhost:8080`. From here: + +- **[[authoring-content|Write content]]** in the `content/` folder +- **[[installation|Push to GitHub]]** with `npx quartz sync` +- **[[hosting|Deploy]]** to GitHub Pages, Cloudflare, Netlify, or Vercel + +For the full walkthrough, see the [[getting-started/index|Getting Started]] guide. + +### Returning User? + +Already have a Quartz repository and cloning it on a new machine? + +```shell +git clone https://github.com//.git +cd +npm ci +npx quartz plugin install +npx quartz build --serve +``` + +> [!tip] +> If you hit build errors on a fresh clone, try `npx quartz plugin install --latest` to refresh plugins to their latest versions. See [[troubleshooting#Plugins fail to build on a fresh clone]] for details. + +## 🔧 Features + +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], [[wikilinks|wikilinks, transclusions]], [[plugins/Backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[features/comments|comments]] and [many more](./features/) right out of the box +- Hot-reload on configuration edits and incremental rebuilds for content edits +- Simple JSX layouts and [[creating components|page components]] +- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes +- Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]] + +For a comprehensive list of features, visit the [features page](./features/). You can read more about the _why_ behind these features on the [[philosophy]] page and a technical overview on the [[architecture]] page. + +### 🚧 Troubleshooting + Updating + +Having trouble with Quartz? Try searching for your issue using the search feature or check the [[troubleshooting]] page. If you haven't already, [[upgrading|upgrade]] to the newest version of Quartz to see if this fixes your issue. + +If you're still having trouble, feel free to [submit an issue](https://github.com/jackyzha0/quartz/issues) if you feel you found a bug or ask for help in our [Discord Community](https://discord.gg/cRFFHYye7t). You can also browse the [[community]] page for third-party plugins and resources. diff --git a/Local/storage/thlab-notes/worker/docs/layout-components.md b/Local/storage/thlab-notes/worker/docs/layout-components.md new file mode 100644 index 0000000..ca8c68f --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/layout-components.md @@ -0,0 +1,194 @@ +--- +title: Higher-Order Layout Components +--- + +Quartz provides several higher-order components that help with layout composition and responsive design. These components wrap other components to add additional functionality or modify their behavior. + +Most common use cases can be configured directly in `quartz.config.yaml` using layout properties. For advanced scenarios requiring custom logic, you can use the TS override approach in `quartz.ts`. + +## `Flex` Component + +The `Flex` component creates a [flexible box layout](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) that can arrange child components in various ways. It's particularly useful for creating responsive layouts and organizing components in rows or columns. + +### YAML Configuration + +In YAML, flex layouts are created using **groups**. Define a group in the top-level `layout.groups` section, then assign plugins to that group via their `layout.group` property: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true # Search will grow to fill available space + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar # Darkmode keeps its natural size + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + +layout: + groups: + toolbar: + direction: row + gap: 0.5rem +``` + +The `groupOptions` field on each plugin entry supports the following flex item properties: + +| Option | Type | Description | +| --------- | --------------------------------------------------------------- | --------------------------------------------------------- | +| `grow` | `boolean` | Whether the component should grow to fill available space | +| `shrink` | `boolean` | Whether the component should shrink if needed | +| `basis` | `string` | Initial main size of the component (e.g., `"200px"`) | +| `order` | `number` | Order in the flex container | +| `align` | `"start"` \| `"end"` \| `"center"` \| `"stretch"` | Cross-axis alignment | +| `justify` | `"start"` \| `"end"` \| `"center"` \| `"between"` \| `"around"` | Main-axis alignment | + +The top-level `layout.groups` section configures the flex container itself: + +| Option | Type | Description | +| ----------- | -------------------------------------------------------------- | ----------------------------------------- | +| `direction` | `"row"` \| `"row-reverse"` \| `"column"` \| `"column-reverse"` | Flex direction | +| `wrap` | `"nowrap"` \| `"wrap"` \| `"wrap-reverse"` | Flex wrap behavior | +| `gap` | `string` | Gap between flex items (e.g., `"0.5rem"`) | + +### TS Override + +For full programmatic control, use the `Component.Flex()` wrapper in `quartz.ts`: + +```ts title="quartz.ts (override)" +Component.Flex({ + components: [ + { + Component: Plugin.Search(), + grow: true, // Search will grow to fill available space + }, + { Component: Plugin.Darkmode() }, // Darkmode keeps its natural size + ], + direction: "row", + gap: "1rem", +}) +``` + +```typescript +type FlexConfig = { + components: { + Component: QuartzComponent + grow?: boolean + shrink?: boolean + basis?: string + order?: number + align?: "start" | "end" | "center" | "stretch" + justify?: "start" | "end" | "center" | "between" | "around" + }[] + direction?: "row" | "row-reverse" | "column" | "column-reverse" + wrap?: "nowrap" | "wrap" | "wrap-reverse" + gap?: string +} +``` + +> [!note] Overriding behavior +> Components inside `Flex` get an additional CSS class `flex-component` that adds the `display: flex` property. If you want to override this behavior, you can add a `display` property to the component's CSS class in your custom CSS file. +> +> ```scss +> .flex-component { +> display: block; // or any other display type +> } +> ``` + +## `MobileOnly` / `DesktopOnly` Components + +These components control whether a plugin is visible on mobile or desktop devices. This is useful for creating responsive layouts where certain components should only appear on specific screen sizes. + +### YAML Configuration + +In YAML, use the `display` property on a plugin's layout entry: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/table-of-contents + enabled: true + layout: + position: right + priority: 20 + display: desktop-only # Only visible on desktop +``` + +Available `display` values: + +| Value | Description | +| -------------- | ------------------------------------- | +| `all` | Visible on all screen sizes (default) | +| `mobile-only` | Only visible on mobile devices | +| `desktop-only` | Only visible on desktop devices | + +### TS Override + +For the TS override approach, use `Component.MobileOnly()` or `Component.DesktopOnly()` wrappers: + +```ts title="quartz.ts (override)" +Component.MobileOnly(Component.Spacer()) +``` + +```ts title="quartz.ts (override)" +Component.DesktopOnly(Plugin.TableOfContents()) +``` + +## `ConditionalRender` Component + +The `ConditionalRender` component conditionally renders a plugin based on page properties. This is useful for creating dynamic layouts where components should only appear under certain conditions. + +### YAML Configuration + +In YAML, use the `condition` property on a plugin's layout entry. Quartz provides several built-in condition presets: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index # Hide breadcrumbs on the root index page +``` + +Available built-in conditions: + +| Condition | Description | +| --------------- | ----------------------------------------------------- | +| `not-index` | Only render when the page is not the root `index.md` | +| `has-tags` | Only render when the page has tags in its frontmatter | +| `has-backlinks` | Only render when the page has backlinks | +| `has-toc` | Only render when the page has a table of contents | + +### TS Override + +For custom conditions that aren't covered by the built-in presets, use `Component.ConditionalRender()` in `quartz.ts`: + +```ts title="quartz.ts (override)" +Component.ConditionalRender({ + component: Plugin.Search(), + condition: (props) => props.displayClass !== "fullpage", +}) +``` + +```typescript +type ConditionalRenderConfig = { + component: QuartzComponent + condition: (props: QuartzComponentProps) => boolean +} +``` + +> [!tip] +> You can also register custom conditions for use in YAML by calling `registerCondition()` in a plugin's initialization code. See [[making plugins]] for more details. diff --git a/Local/storage/thlab-notes/worker/docs/layout.md b/Local/storage/thlab-notes/worker/docs/layout.md new file mode 100644 index 0000000..b44bcf7 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/layout.md @@ -0,0 +1,240 @@ +--- +title: Layout +--- + +Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To enable easy customization, these emitters allow you to fully rearrange the layout of the page. + +In v5, the layout is defined in `quartz.config.yaml`. Each plugin controls its own layout position via `layout.position` and `layout.priority` fields. The top-level `layout` section provides two additional mechanisms: + +- `layout.groups` defines flex containers (like `toolbar`) that group multiple components into a single row or column. See [[layout-components]] for details. +- `layout.byPageType` contains per-page-type overrides (content, folder, tag, 404) for beforeBody, left, right sections, and optionally a `template` to control the page's [[#Page Frames|page frame]]. + +Each page is composed of multiple different sections which contain `QuartzComponents`. The following code snippet lists all of the valid sections that you can add components to: + +```typescript title="quartz/cfg.ts" +export interface FullPageLayout { + head: QuartzComponent // single component + header: QuartzComponent[] // laid out horizontally + beforeBody: QuartzComponent[] // laid out vertically + pageBody: QuartzComponent // single component + afterBody: QuartzComponent[] // laid out vertically + left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile + right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile + footer: QuartzComponent // single component +} +``` + +These correspond to following parts of the page: + +| Layout | Preview | +| ------------------------------- | ----------------------------------- | +| Desktop (width > 1200px) | ![[quartz-layout-desktop.png\|800]] | +| Tablet (800px < width < 1200px) | ![[quartz-layout-tablet.png\|800]] | +| Mobile (width < 800px) | ![[quartz-layout-mobile.png\|800]] | + +> [!note] +> There are two additional layout fields that are _not_ shown in the above diagram. +> +> 1. `head` is a single component that renders the `` [tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) in the HTML. This doesn't appear visually on the page and is only is responsible for metadata about the document like the tab title, scripts, and styles. +> 2. `header` is a set of components that are laid out horizontally and appears _before_ the `beforeBody` section. This enables you to replicate the old Quartz 3 header bar where the title, search bar, and dark mode toggle. By default, Quartz doesn't place any components in the `header`. + +Layout components are configured in the `layout` section of `quartz.config.yaml`. Plugins declare their position and priority, and the layout system arranges them automatically: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 30 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: true + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + +layout: + groups: + toolbar: + direction: row + gap: 0.5rem + byPageType: + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + "404": + positions: + beforeBody: [] + left: [] + right: [] +``` + +### Conditional Rendering + +Plugins can specify a `condition` in their layout block to control when they appear. This uses built-in presets: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index +``` + +Available conditions: + +| Condition | Effect | +| ----------- | ---------------------------------------------------- | +| `not-index` | Hidden on the root index page, shown everywhere else | +| `has-tags` | Only shown on pages that have tags in frontmatter | + +See [[layout-components]] for more details on conditional rendering and display options. + +For advanced layout overrides using TypeScript (e.g. custom component wrappers or conditional logic), you can use the TS override in `quartz.ts`: + +```ts title="quartz.ts" +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout({ + defaults: { + // override default layout for all page types + }, + byPageType: { + content: { + // override layout for content pages only + }, + folder: { + // override layout for folder pages only + }, + }, +}) +``` + +Fields defined in `defaults` can be overridden by specific entries in `byPageType`. + +Community component plugins are installed via `npx quartz plugin add github:quartz-community/`. See [[layout-components]] for built-in layout utilities (Flex, MobileOnly, DesktopOnly, etc.). + +You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz. + +### Page Frames + +Page frames control the overall HTML structure of a page — specifically, how the layout slots (sidebars, header, content, footer) are arranged inside the page shell. Different page types can use different frames to produce fundamentally different layouts. + +Quartz ships with three built-in frames: + +| Frame | Description | Used by | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| `default` | Three-column layout with left sidebar, center content (header, beforeBody, content, afterBody), right sidebar, and footer. This is the standard Quartz layout. | ContentPage, FolderPage, TagPage, BasesPage | +| `full-width` | No sidebars. Single center column spanning the full width with header, content, afterBody, and footer. | — | +| `minimal` | No sidebars, no header or beforeBody chrome. Only content and footer. | NotFoundPage (404) | + +Plugins can also provide their own frames. For example, the `canvas-page` plugin ships a `"canvas"` frame that provides a fullscreen canvas with a togglable sidebar. + +#### How frames are resolved + +Each page type can declare a default frame in its plugin source code via the `frame` property. The resolution order is: + +1. **YAML config override**: `layout.byPageType..template` in `quartz.config.yaml` +2. **Plugin-registered frame**: Frames registered by plugins via the Frame Registry (loaded from the plugin's `frames` export) +3. **Plugin declaration**: The `frame` property set in the page type plugin's source code +4. **Fallback**: `"default"` + +For example, to override canvas pages to use the minimal frame: + +```yaml title="quartz.config.yaml" +layout: + byPageType: + canvas: + template: minimal +``` + +#### Custom frames + +There are two ways to provide custom frames: + +**1. Plugin-provided frames (recommended for reusable frames):** + +Plugins can ship their own frames by declaring them in `package.json` and exporting them from a `./frames` subpath. See [[making plugins#Providing Custom Frames|the plugin guide]] for details. When a plugin with frames is installed, its frames are automatically registered in the Frame Registry and available by name. + +**2. Core frames (for project-specific frames):** + +You can also create frames directly in `quartz/components/frames/` by implementing the `PageFrame` interface and registering the frame in `quartz/components/frames/index.ts`. See the [[architecture|architecture overview]] for the full `PageFrame` interface. + +Frames are applied as a `data-frame` attribute on the `.page` element, which you can target in CSS: + +```scss +.page[data-frame="my-frame"] > #quartz-body { + /* custom grid layout */ +} +``` + +Frame CSS should be scoped with `[data-frame="name"]` selectors to avoid conflicts with other frames. + +### Layout breakpoints + +Quartz has different layouts depending on the width the screen viewing the website. + +The breakpoints for layouts can be configured in `variables.scss`. + +- `mobile`: screen width below this size will use mobile layout. +- `desktop`: screen width above this size will use desktop layout. +- Screen width between `mobile` and `desktop` width will use the tablet layout. + +```scss +$breakpoints: ( + mobile: 800px, + desktop: 1200px, +); +``` + +### Style + +Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz uses [Sass](https://sass-lang.com/guide/) for styling. + +You can see the base style sheet in `quartz/styles/base.scss` and write your own in `quartz/styles/custom.scss`. + +> [!note] +> Some components may provide their own styling as well! Community plugins bundle their own styles. If you'd like to customize styling for a specific component, double check the component definition to see how its styles are defined. diff --git a/Local/storage/thlab-notes/worker/docs/philosophy.md b/Local/storage/thlab-notes/worker/docs/philosophy.md new file mode 100644 index 0000000..5eea400 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/philosophy.md @@ -0,0 +1,47 @@ +--- +title: Philosophy of Quartz +--- + +## A garden should be a true hypertext + +> The garden is the web as topology. Every walk through the garden creates new paths, new meanings, and when we add things to the garden we add them in a way that allows many future, unpredicted relationships. +> +> _(The Garden and the Stream)_ + +The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes? + +The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking. + +My goal with a digital garden is not purely as an organizing system and information store (though it works nicely for that). I want my digital garden to be a playground for new ways ideas can connect together. As a result, existing formal organizing systems like Zettelkasten or the hierarchical folder structures of Notion don’t work well for me. There is way too much upfront friction that by the time I’ve thought about how to organize my thought into folders categories, I’ve lost it. + +Quartz embraces the inherent rhizomatic and web-like nature of our thinking and tries to encourage note-taking in a similar form. + +--- + +## A garden should be shared + +The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea. + +Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing. + +> “[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 + +**The goal of Quartz is to make sharing your digital garden free and simple.** + +--- + +## A garden should be your own + +At its core, Quartz is designed to be easy to use enough for non-technical people to get going but also powerful enough that senior developers can tweak it to work how they'd like it to work. + +1. If you like the default configuration of Quartz and just want to change the content, the only thing that you need to change is the contents of the `content` folder. +2. If you'd like to make basic configuration tweaks but don't want to edit source code, one can tweak the plugins and components in `quartz.config.yaml` in a guided manner to their liking. +3. If you'd like to tweak the actual source code of the underlying plugins, components, or even build process, Quartz purposefully ships its full source code to the end user to allow customization at this level too. + +Most software either confines you to either + +1. Makes it easy to tweak content but not the presentation +2. Gives you too many knobs to tune the presentation without good opinionated defaults + +**Quartz should feel powerful but ultimately be an intuitive tool fully within your control.** It should be a piece of [agentic software](https://jzhao.xyz/posts/agentic-computing). Ultimately, it should have the right affordances to nudge users towards good defaults but never dictate what the 'correct' way of using it is. diff --git a/Local/storage/thlab-notes/worker/docs/plugins/AliasRedirects.md b/Local/storage/thlab-notes/worker/docs/plugins/AliasRedirects.md new file mode 100644 index 0000000..8e53e62 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/AliasRedirects.md @@ -0,0 +1,68 @@ +--- +title: AliasRedirects +description: Generates redirect pages from frontmatter aliases and case-preserving URLs. +tags: + - plugin/emitter +image: https://images.unsplash.com/photo-1601735479770-bb5de9dbe844 +repository: "[quartz-community/alias-redirects](https://github.com/quartz-community/alias-redirects)" +enabled: true +required: false +--- + +This plugin emits HTML redirect pages so that old URLs redirect to the canonical page. It handles two types of redirects: + +1. **Frontmatter aliases**: Redirect pages for aliases defined in your content's frontmatter. +2. **Case-preserving redirects**: Automatic redirect pages for URLs that changed due to Quartz v5's lowercase slug normalization. + +### Frontmatter Aliases + +If `foo.md` has the following frontmatter: + +```md title="foo.md" +--- +title: "Foo" +aliases: + - "bar" +--- +``` + +The target `host.me/bar` will be permanently redirected to `host.me/foo`. + +The emitter supports the following frontmatter fields: + +- `aliases` +- `alias` + +### Case-Preserving Redirects + +Quartz v5 normalizes all URLs to lowercase. If you are migrating from v4 (which preserved the original casing), previously indexed URLs containing uppercase letters (e.g. `/Diary/My-Note`) would return 404 errors. + +When `enableCaseRedirects` is enabled (the default), this plugin automatically detects files whose original path differs from the lowercased slug and generates redirect pages at the original-case URL. For example, if your content directory contains `Diary/2026-01-01.md`, the plugin generates: + +- The canonical page at `/diary/2026-01-01` (produced by the normal build) +- A redirect page at `/Diary/2026-01-01` (produced by this plugin) + +The redirect page includes proper SEO signals: + +- `` for an instant redirect +- `` pointing to the lowercase URL +- `` to prevent duplicate indexing + +This preserves search engine rankings and ensures inbound links continue to work. + +> [!note] +> Case-preserving redirects have no effect on case-insensitive filesystems (macOS, Windows) where the server already resolves either casing to the same file. The plugin automatically detects the filesystem type and skips redirect generation when unnecessary. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `enableCaseRedirects`: If `true` (default), automatically generates redirect pages for URLs that changed casing due to v5's lowercase normalization. Set to `false` to disable this behavior. + +## API + +- Category: Emitter +- Function name: `ExternalPlugin.AliasRedirects()`. +- Source: [`quartz-community/alias-redirects`](https://github.com/quartz-community/alias-redirects) +- Install: `npx quartz plugin add github:quartz-community/alias-redirects` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ArticleTitle.md b/Local/storage/thlab-notes/worker/docs/plugins/ArticleTitle.md new file mode 100644 index 0000000..0802c2e --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ArticleTitle.md @@ -0,0 +1,24 @@ +--- +title: ArticleTitle +description: Renders the article title as an h1 heading. +tags: + - plugin/component +image: +repository: "[quartz-community/article-title](https://github.com/quartz-community/article-title)" +enabled: true +required: false +--- + +This plugin renders the article title from the page's frontmatter as an `

` heading at the top of the page content. It reads the `title` field from frontmatter (falling back to the filename if no title is set). + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Component +- Function name: `ExternalPlugin.ArticleTitle()`. +- Source: [`quartz-community/article-title`](https://github.com/quartz-community/article-title) +- Install: `npx quartz plugin add github:quartz-community/article-title` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Assets.md b/Local/storage/thlab-notes/worker/docs/plugins/Assets.md new file mode 100644 index 0000000..57b41d8 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Assets.md @@ -0,0 +1,21 @@ +--- +title: Assets +tags: + - plugin/emitter +image: https://images.unsplash.com/photo-1526304640581-d334cdbbf45e +--- + +This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` in the global [[configuration]]. + +Note that all static assets will then be accessible through its path on your generated site, i.e: `host.me/path/to/static.pdf` + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `Plugin.Assets()` (internal plugin). +- Source: [`quartz/plugins/emitters/assets.ts`](https://github.com/jackyzha0/quartz/blob/v5/quartz/plugins/emitters/assets.ts). diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Backlinks.md b/Local/storage/thlab-notes/worker/docs/plugins/Backlinks.md new file mode 100644 index 0000000..e93fdb9 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Backlinks.md @@ -0,0 +1,39 @@ +--- +title: Backlinks +description: Shows pages that link to the current page. +tags: + - plugin/component +image: +repository: "[quartz-community/backlinks](https://github.com/quartz-community/backlinks)" +enabled: true +required: false +--- + +Shows pages that link to the current page. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[plugins/Backlinks]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `hideWhenEmpty`: Hide the backlinks section if the current page has no backlinks. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/backlinks + enabled: true + options: + hideWhenEmpty: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Backlinks()`. +- Source: [`quartz-community/backlinks`](https://github.com/quartz-community/backlinks) +- Install: `npx quartz plugin add github:quartz-community/backlinks` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/BasesPage.md b/Local/storage/thlab-notes/worker/docs/plugins/BasesPage.md new file mode 100644 index 0000000..6454d58 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/BasesPage.md @@ -0,0 +1,75 @@ +--- +title: BasesPage +description: Renders Obsidian Bases files as database-style views. +tags: + - plugin/pageType + - plugin/component +image: +new-in-v5: true +repository: "[quartz-community/bases-page](https://github.com/quartz-community/bases-page)" +enabled: true +required: false +--- + +This plugin provides support for [Obsidian Bases](https://obsidian.md/changelog/2025-04-15-desktop-v1.8.0/) (`.base` files) in Quartz. It reads `.base` files from your vault, resolves matching notes based on the query definition, and renders them as interactive database-like views with support for tables, lists, cards, and maps. It uses the `default` [[layout#Page Frames|page frame]] (three-column layout with sidebars). + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Features + +- **Table view**: Sortable columns with automatic type rendering (strings, numbers, booleans, arrays, links). +- **List view**: Compact list with metadata chips for each entry. +- **Cards view**: Card layout with optional image property support. +- **Map view**: Placeholder for future map-based visualization. +- **Multiple views**: A single `.base` file can define multiple views, displayed as switchable tabs. +- **Filters**: Recursive filter trees with `and`/`or`/`not` operators. +- **Formulas**: Computed properties via formula expressions. +- **Summaries**: Column-level aggregations (Sum, Average, Min, Max, Median, etc.). +- **Property configuration**: Custom display names for properties. +- **Link rendering**: Wikilinks and Markdown links within cell values are rendered as clickable links. + +## Interaction with `unlisted` pages + +`BasesPage` respects the `file.data.unlisted` convention written by [[UnlistedPages]] and [[EncryptedPages]]. Pages marked `unlisted: true` (or encrypted pages with `stealth: true`) are excluded from every rendered base view — table, list, board, cards, gallery, and any custom view — regardless of whether the base's filter expression would match them. Unlisted pages also cannot be dereferenced from formulas on visible pages via `.asFile()`. + +> [!note] +> Base views are **server-side rendered** HTML baked at build time. They do not update client-side after a visitor decrypts an encrypted page. Graph, explorer, and search all re-hydrate from the patched in-memory content index and show newly-unlocked pages for the rest of the browser session — base views do not, because they were materialized at build time with unlisted pages already excluded. A visitor who successfully decrypts a revealable encrypted page will see it appear in graph, explorer, and search, but **not** in any base view, until the site is rebuilt with that page listed. This is the same structural limitation that applies to backlinks, recent notes, folder listings, and tag listings. + +## Configuration + +This plugin accepts the following configuration options: + +- `defaultViewType`: The default view type when none is specified in the `.base` file. Defaults to `"table"`. +- `linkResolution`: How to resolve internal links in view renderers. Should match the `markdownLinkResolution` setting of the [[CrawlLinks]] plugin. Can be `"absolute"`, `"relative"`, or `"shortest"`. Defaults to `"shortest"`. +- `customViews`: A map of custom view renderers. Keys are view type names. These override built-in renderers for the same type, or add new view types. Requires a TS override. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/bases-page + enabled: true +``` + +For custom view renderers, use a TS override in `quartz.ts`: + +```ts title="quartz.ts (override)" +import * as ExternalPlugin from "./.quartz/plugins" + +// Must be placed before loadQuartzConfig() +ExternalPlugin.BasesPage({ + defaultViewType: "table", + customViews: { + myView: ({ entries, view, basesData, total, locale }) => { + // return JSX + }, + }, +}) +``` + +## API + +- Category: Page Type, Component +- Function name: `ExternalPlugin.BasesPage()`. +- Source: [`quartz-community/bases-page`](https://github.com/quartz-community/bases-page) +- Install: `npx quartz plugin add github:quartz-community/bases-page` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Breadcrumbs.md b/Local/storage/thlab-notes/worker/docs/plugins/Breadcrumbs.md new file mode 100644 index 0000000..beb98a6 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Breadcrumbs.md @@ -0,0 +1,45 @@ +--- +title: Breadcrumbs +description: Breadcrumb navigation trail. +tags: + - plugin/component +image: +repository: "[quartz-community/breadcrumbs](https://github.com/quartz-community/breadcrumbs)" +enabled: true +required: false +--- + +Navigation breadcrumb trail. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[plugins/Breadcrumbs]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `spacerSymbol`: The symbol to use between breadcrumb items. Defaults to `"❯"`. +- `rootName`: The name of the root page. Defaults to `Home`. +- `resolveFrontmatterTitle`: Whether to use the `title` frontmatter field for breadcrumb items. Defaults to `true`. +- `showCurrentPage`: Whether to show the current page in the breadcrumb trail. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/breadcrumbs + enabled: true + options: + spacerSymbol: "❯" + rootName: Home + resolveFrontmatterTitle: true + showCurrentPage: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Breadcrumbs()`. +- Source: [`quartz-community/breadcrumbs`](https://github.com/quartz-community/breadcrumbs) +- Install: `npx quartz plugin add github:quartz-community/breadcrumbs` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/CNAME.md b/Local/storage/thlab-notes/worker/docs/plugins/CNAME.md new file mode 100644 index 0000000..c817e3a --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/CNAME.md @@ -0,0 +1,28 @@ +--- +title: CNAME +description: Emits a CNAME file for custom domain deployment. +tags: + - plugin/emitter +image: +repository: "[quartz-community/cname](https://github.com/quartz-community/cname)" +enabled: true +required: false +--- + +This plugin emits a `CNAME` record that points your subdomain to the default domain of your site. + +If you want to use a custom domain name like `quartz.example.com` for the site, then this is needed. + +See [[hosting|Hosting]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `ExternalPlugin.CNAME()`. +- Source: [`quartz-community/cname`](https://github.com/quartz-community/cname) +- Install: `npx quartz plugin add github:quartz-community/cname` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/CanvasPage.md b/Local/storage/thlab-notes/worker/docs/plugins/CanvasPage.md new file mode 100644 index 0000000..9c98476 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/CanvasPage.md @@ -0,0 +1,59 @@ +--- +title: CanvasPage +description: Renders JSON Canvas files as interactive, pannable pages. +tags: + - plugin/pageType +image: "#7852ee" +new-in-v5: true +repository: "[quartz-community/canvas-page](https://github.com/quartz-community/canvas-page)" +enabled: true +required: false +--- + +This plugin is a page type plugin that renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It uses a custom `"canvas"` [[layout#Page Frames|page frame]] that provides a fullscreen, always-on canvas experience with a togglable left sidebar for navigation. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/), including text nodes with Markdown rendering, file nodes that link to other pages in your vault, link nodes for external URLs, and group nodes for visual organization. Edges between nodes are rendered as SVG paths with optional labels, arrow markers, and colors. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `enableInteraction`: Whether to enable pan and zoom interaction on the canvas. Default: `true{:ts}`. +- `initialZoom`: The initial zoom level when the canvas is first displayed. Default: `1{:ts}`. +- `minZoom`: The minimum zoom level allowed when zooming out. Default: `0.1{:ts}`. +- `maxZoom`: The maximum zoom level allowed when zooming in. Default: `5{:ts}`. + +### Canvas Frame + +The canvas-page plugin provides its own `"canvas"` page frame via the [[layout#Page Frames|Frame Registry]]. This frame: + +- Renders the canvas in **fullscreen mode** by default (100vw × 100vh), giving the canvas maximum screen space — leaning into the "endless canvas" concept of JSON Canvas. +- Provides a **togglable left sidebar** that slides in from the left edge. This is the only layout slot available — it renders the same components as the `left` sidebar on content pages (e.g., Explorer, Search, Page Title). +- The sidebar toggle button (hamburger/close icon) is positioned in the top-left corner. +- Canvas controls (zoom in, zoom out, reset) are positioned on the right side. +- On mobile, the sidebar overlays the canvas rather than pushing it aside. + +Users can override this frame via `quartz.config.yaml` if needed: + +```yaml title="quartz.config.yaml" +layout: + byPageType: + canvas: + template: default # Use standard three-column layout instead +``` + +### Features + +- **Text nodes**: Render Markdown content including headings, bold, italic, strikethrough, lists, links, and code blocks via [GFM](https://github.github.com/gfm/) support. +- **File nodes**: Link to other pages in your vault. Supports popover previews on hover. +- **Link nodes**: Reference external URLs. +- **Group nodes**: Visual grouping containers with optional labels and background colors. +- **Edges**: SVG connections between nodes with optional labels, arrow markers, and colors. Supports all four sides (top, right, bottom, left) and both preset colors (1–6) and custom hex colors. +- **Togglable sidebar**: Hamburger button in the top-left corner toggles the left sidebar for navigation. Press `Escape` or click the close button to dismiss. +- **Preset colors**: Six preset colors (red, orange, yellow, green, cyan, purple) plus custom hex colors (`#RRGGBB`) for nodes and edges. + +## API + +- Category: Page Type +- Function name: `ExternalPlugin.CanvasPage()`. +- Source: [`quartz-community/canvas-page`](https://github.com/quartz-community/canvas-page) +- Install: `npx quartz plugin add github:quartz-community/canvas-page` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Citations.md b/Local/storage/thlab-notes/worker/docs/plugins/Citations.md new file mode 100644 index 0000000..f04ffc7 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Citations.md @@ -0,0 +1,29 @@ +--- +title: Citations +description: Academic citation and bibliography support via BibTeX. +tags: + - plugin/transformer +image: https://images.unsplash.com/photo-1582079133805-43655f026448 +repository: "[quartz-community/citations](https://github.com/quartz-community/citations)" +enabled: false +required: false +--- + +This plugin adds Citation support to Quartz. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `bibliographyFile`: the path to the bibliography file. Defaults to `./bibliography.bib`. This is relative to git source of your vault. +- `suppressBibliography`: whether to suppress the bibliography at the end of the document. Defaults to `false`. +- `linkCitations`: whether to link citations to the bibliography. Defaults to `false`. +- `csl`: the citation style to use. Defaults to `apa`. Reference [rehype-citation](https://rehype-citation.netlify.app/custom-csl) for more options. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.Citations()`. +- Source: [`quartz-community/citations`](https://github.com/quartz-community/citations) +- Install: `npx quartz plugin add github:quartz-community/citations` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Comments.md b/Local/storage/thlab-notes/worker/docs/plugins/Comments.md new file mode 100644 index 0000000..a79fb8e --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Comments.md @@ -0,0 +1,58 @@ +--- +title: Comments +description: Comment system integration (Giscus, Utterances, etc.). +tags: + - plugin/component +image: "[[giscus-results.png]]" +repository: "[quartz-community/comments](https://github.com/quartz-community/comments)" +enabled: false +required: false +--- + +Comment system (giscus, utterances, etc.). + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[plugins/Comments]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `provider`: The comment provider to use. Currently only `giscus` is supported. +- `options`: Provider-specific options. + - `repo`: The GitHub repository to use for comments. + - `repoId`: The ID of the GitHub repository. + - `category`: The discussion category to use. + - `categoryId`: The ID of the discussion category. + - `lang`: The language for the comment system. Defaults to `en`. + - `themeUrl`: URL to a folder with custom themes. + - `lightTheme`: Filename for the light theme CSS file. Defaults to `light`. + - `darkTheme`: Filename for the dark theme CSS file. Defaults to `dark`. + - `mapping`: How to map pages to discussions. Defaults to `url`. + - `strict`: Use strict title matching. Defaults to `true`. + - `reactionsEnabled`: Whether to enable reactions for the main post. Defaults to `true`. + - `inputPosition`: Where to put the comment input box relative to the comments. Defaults to `bottom`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/comments + enabled: true + options: + provider: giscus + options: + repo: jackyzha0/quartz + repoId: MDEwOlJlcG9zaXRvcnkzODcyMTMyMDg + category: Announcements + categoryId: DIC_kwDOFxRnmM4B-Xg6 + lang: en +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Comments()`. +- Source: [`quartz-community/comments`](https://github.com/quartz-community/comments) +- Install: `npx quartz plugin add github:quartz-community/comments` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ComponentResources.md b/Local/storage/thlab-notes/worker/docs/plugins/ComponentResources.md new file mode 100644 index 0000000..3d60c84 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ComponentResources.md @@ -0,0 +1,19 @@ +--- +title: ComponentResources +tags: + - plugin/emitter +image: +--- + +This plugin manages and emits the static resources required for the Quartz framework. This includes CSS stylesheets and JavaScript scripts that enhance the functionality and aesthetics of the generated site. See also the `cdnCaching` option in the `theme` section of the [[configuration]]. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `Plugin.ComponentResources()` (internal plugin). +- Source: [`quartz/plugins/emitters/componentResources.ts`](https://github.com/jackyzha0/quartz/blob/v5/quartz/plugins/emitters/componentResources.ts). diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ContentIndex.md b/Local/storage/thlab-notes/worker/docs/plugins/ContentIndex.md new file mode 100644 index 0000000..e2b7489 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ContentIndex.md @@ -0,0 +1,33 @@ +--- +title: ContentIndex +description: Generates sitemap, RSS feed, and content index. +tags: + - plugin/emitter +image: +repository: "[quartz-community/content-index](https://github.com/quartz-community/content-index)" +enabled: true +required: false +--- + +This plugin emits both RSS and an XML sitemap for your site. The [[RSS Feed]] allows users to subscribe to content on your site and the sitemap allows search engines to better index your site. The plugin also emits a `contentIndex.json` file which is used by dynamic frontend components like search and graph. + +This plugin emits a comprehensive index of the site's content, generating additional resources such as a sitemap, an RSS feed, and a + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `enableSiteMap`: If `true` (default), generates a sitemap XML file (`sitemap.xml`) listing all site URLs for search engines in content discovery. +- `enableRSS`: If `true` (default), produces an RSS feed (`index.xml`) with recent content updates. +- `rssLimit`: Defines the maximum number of entries to include in the RSS feed, helping to focus on the most recent or relevant content. Defaults to `10`. +- `rssFullHtml`: If `true`, the RSS feed includes the full rendered HTML content of each page. Defaults to `false`. +- `rssSlug`: Slug to the generated RSS feed XML file. Defaults to `"index"`. +- `includeEmptyFiles`: If `true` (default), content files with no body text are included in the generated index and resources. + +## API + +- Category: Emitter +- Function name: `ExternalPlugin.ContentIndex()`. +- Source: [`quartz-community/content-index`](https://github.com/quartz-community/content-index) +- Install: `npx quartz plugin add github:quartz-community/content-index` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ContentMeta.md b/Local/storage/thlab-notes/worker/docs/plugins/ContentMeta.md new file mode 100644 index 0000000..22d023d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ContentMeta.md @@ -0,0 +1,39 @@ +--- +title: ContentMeta +description: Displays creation date and reading time. +tags: + - plugin/component +image: +repository: "[quartz-community/content-meta](https://github.com/quartz-community/content-meta)" +enabled: true +required: false +--- + +This plugin displays content metadata below the article title, such as the creation date and estimated reading time. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Configuration + +This plugin accepts the following configuration options: + +- `showReadingTime`: Whether to display the estimated reading time. Defaults to `true`. +- `showComma`: Whether to display a comma between metadata items. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/content-meta + enabled: true + options: + showReadingTime: true + showComma: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.ContentMeta()`. +- Source: [`quartz-community/content-meta`](https://github.com/quartz-community/content-meta) +- Install: `npx quartz plugin add github:quartz-community/content-meta` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ContentPage.md b/Local/storage/thlab-notes/worker/docs/plugins/ContentPage.md new file mode 100644 index 0000000..ca3d25c --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ContentPage.md @@ -0,0 +1,24 @@ +--- +title: ContentPage +description: Generates HTML pages for Markdown content. +tags: + - plugin/pageType +image: +repository: "[quartz-community/content-page](https://github.com/quartz-community/content-page)" +enabled: true +required: false +--- + +This plugin is a page type plugin for the Quartz framework. It generates the HTML pages for each piece of Markdown content. It emits the full-page [[layout]], including headers, footers, and body content, among others. It uses the `default` [[layout#Page Frames|page frame]] (three-column layout with sidebars). It is now configured in the `pageTypes` section of `quartz.config.yaml`. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Page Type +- Function name: `ExternalPlugin.ContentPage()`. +- Source: [`quartz-community/content-page`](https://github.com/quartz-community/content-page) +- Install: `npx quartz plugin add github:quartz-community/content-page` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/CrawlLinks.md b/Local/storage/thlab-notes/worker/docs/plugins/CrawlLinks.md new file mode 100644 index 0000000..bf161e4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/CrawlLinks.md @@ -0,0 +1,37 @@ +--- +title: CrawlLinks +description: Parses and resolves internal links. Removing it is not recommended. +tags: + - plugin/transformer +image: +repository: "[quartz-community/crawl-links](https://github.com/quartz-community/crawl-links)" +enabled: true +required: true +--- + +This plugin parses links and processes them to point to the right places. It is also needed for embedded links (like images). See [[Obsidian compatibility]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `markdownLinkResolution`: Sets the strategy for resolving Markdown paths, can be `"absolute"` (default), `"relative"` or `"shortest"`. You should use the same setting here as in [[Obsidian compatibility|Obsidian]]. + - `absolute`: Path relative to the root of the content folder. + - `relative`: Path relative to the file you are linking from. + - `shortest`: Name of the file. If this isn't enough to identify the file, use the full absolute path. +- `prettyLinks`: If `true` (default), simplifies links by removing folder paths, making them more user friendly (e.g. `folder/deeply/nested/note` becomes `note`). +- `openLinksInNewTab`: If `true`, configures external links to open in a new tab. Defaults to `false`. +- `lazyLoad`: If `true`, adds lazy loading to resource elements (`img`, `video`, etc.) to improve page load performance. Defaults to `false`. +- `externalLinkIcon`: Adds an icon next to external links when `true` (default) to visually distinguishing them from internal links. +- `disableBrokenWikilinks`: If `true`, internal links whose resolved slug is not present in the site (i.e. no matching file under `markdownLinkResolution`) gain an additional `broken` CSS class alongside `internal`, so they can be styled distinctly. Defaults to `false`. Applies to both wikilinks and regular Markdown links, since both are indistinguishable `` elements by the time this plugin runs. + +> [!warning] +> Removing this plugin is _not_ recommended and will likely break the page. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.CrawlLinks()`. +- Source: [`quartz-community/crawl-links`](https://github.com/quartz-community/crawl-links) +- Install: `npx quartz plugin add github:quartz-community/crawl-links` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/CreatedModifiedDate.md b/Local/storage/thlab-notes/worker/docs/plugins/CreatedModifiedDate.md new file mode 100644 index 0000000..0e93197 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/CreatedModifiedDate.md @@ -0,0 +1,34 @@ +--- +title: CreatedModifiedDate +description: Determines creation and modification dates from frontmatter, git, or filesystem. +tags: + - plugin/transformer +image: +repository: "[quartz-community/created-modified-date](https://github.com/quartz-community/created-modified-date)" +enabled: true +required: false +--- + +This plugin determines the created, modified, and published dates for a document using three potential data sources: frontmatter metadata, Git history, and the filesystem. See [[authoring content#Syntax]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `priority`: The data sources to consult for date information. Highest priority first. Possible values are `"frontmatter"`, `"git"`, and `"filesystem"`. Defaults to `["frontmatter", "git", "filesystem"]`. +- `defaultDateType`: Which date type to use when displaying dates. Can be `"created"`, `"modified"`, or `"published"`. Defaults to `"modified"`. + +When loading the frontmatter, the value of [[Frontmatter#List]] is used. + +> [!warning] +> If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in the plugin's options. +> +> Depending on how you [[hosting|host]] your Quartz, the `filesystem` dates of your local files may not match the final dates. In these cases, it may be better to use `git` or `frontmatter` to guarantee correct dates. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.CreatedModifiedDate()`. +- Source: [`quartz-community/created-modified-date`](https://github.com/quartz-community/created-modified-date) +- Install: `npx quartz plugin add github:quartz-community/created-modified-date` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/CustomOgImages.md b/Local/storage/thlab-notes/worker/docs/plugins/CustomOgImages.md new file mode 100644 index 0000000..59f8a2d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/CustomOgImages.md @@ -0,0 +1,383 @@ +--- +title: Custom OG Images +description: Generates Open Graph social preview images. +tags: + - feature/emitter +image: "[[social-image-preview-dark.png]]" +repository: "[quartz-community/og-image](https://github.com/quartz-community/og-image)" +enabled: true +required: false +--- + +The Custom OG Images emitter plugin generates social media preview images for your pages. It uses [satori](https://github.com/vercel/satori) to convert HTML/CSS into images, allowing you to create beautiful and consistent social media preview cards for your content. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Features + +- Automatically generates social media preview images for each page +- Supports both light and dark mode themes +- Customizable through frontmatter properties +- Fallback to default image when needed +- Full control over image design through custom components + +## Configuration + +> [!info] Info +> +> The `baseUrl` property in your [[configuration]] must be set properly for social images to work correctly, as they require absolute paths. + +This plugin accepts the following configuration options: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/og-image + enabled: true + options: + colorScheme: lightMode # "lightMode" or "darkMode" + width: 1200 + height: 630 + excludeRoot: false +``` + +For the TS override approach (needed for custom `imageStructure`): + +```ts title="quartz.ts (override)" +import * as ExternalPlugin from "./.quartz/plugins" +import { defaultImage } from "./quartz/plugins/emitters/ogImage" + +// Must be placed before loadQuartzConfig() +ExternalPlugin.CustomOgImages({ + colorScheme: "lightMode", + width: 1200, + height: 630, + excludeRoot: false, + imageStructure: defaultImage, +}) +``` + +### Configuration Options + +| Option | Type | Default | Description | +| -------------------- | --------- | ------------------------- | ----------------------------------------------------------------- | +| `colorScheme` | string | "lightMode" | Theme to use for generating images ("darkMode" or "lightMode") | +| `width` | number | 1200 | Width of the generated image in pixels | +| `height` | number | 630 | Height of the generated image in pixels | +| `excludeRoot` | boolean | false | Whether to exclude the root index page from auto-generated images | +| `defaultTitle` | string | "Untitled" | Fallback title when a page has no title | +| `defaultDescription` | string | "No description provided" | Fallback description when a page has no description | +| `imageStructure` | component | defaultImage | Custom component to use for image generation | + +## Frontmatter Properties + +The following properties can be used to customize your link previews: + +| Property | Alias | Summary | +| ------------------- | ---------------- | ----------------------------------- | +| `socialDescription` | `description` | Description to be used for preview. | +| `socialImage` | `image`, `cover` | Link to preview image. | + +The `socialImage` property should contain a link to an image either relative to `quartz/static`, or a full URL. If you have a folder for all your images in `quartz/static/my-images`, an example for `socialImage` could be `"my-images/cover.png"`. Alternatively, you can use a fully qualified URL like `"https://example.com/cover.png"`. + +> [!info] Info +> +> The priority for what image will be used for the cover image looks like the following: `frontmatter property > generated image (if enabled) > default image`. +> +> The default image (`quartz/static/og-image.png`) will only be used as a fallback if nothing else is set. If the Custom OG Images emitter plugin is enabled, it will be treated as the new default per page, but can be overwritten by setting the `socialImage` frontmatter property for that page. + +## Customization + +You can fully customize how the images being generated look by passing your own component to `imageStructure`. This component takes JSX + some page metadata/config options and converts it to an image using [satori](https://github.com/vercel/satori). Vercel provides an [online playground](https://og-playground.vercel.app/) that can be used to preview how your JSX looks like as a picture. This is ideal for prototyping your custom design. + +### Fonts + +You will also be passed an array containing a header and a body font (where the first entry is header and the second is body). The fonts matches the ones selected in `theme.typography.header` and `theme.typography.body` from `quartz.config.yaml` and will be passed in the format required by [`satori`](https://github.com/vercel/satori). To use them in CSS, use the `.name` property (e.g. `fontFamily: fonts[1].name` to use the "body" font family). + +An example of a component using the header font could look like this: + +```tsx title="socialImage.tsx" +export const myImage: SocialImageOptions["imageStructure"] = (...) => { + return

Cool Header!

+} +``` + +> [!example]- Local fonts +> +> For cases where you use a local fonts under `static` folder, make sure to set the correct `@font-face` in `custom.scss` +> +> ```scss title="custom.scss" +> @font-face { +> font-family: "Newsreader"; +> font-style: normal; +> font-weight: normal; +> font-display: swap; +> src: url("/static/Newsreader.woff2") format("woff2"); +> } +> ``` +> +> Then in `quartz/util/og.tsx`, you can load the Satori fonts like so: +> +> ```tsx title="quartz/util/og.tsx" +> import { joinSegments, QUARTZ } from "../path" +> import fs from "fs" +> import path from "path" +> +> const newsreaderFontPath = joinSegments(QUARTZ, "static", "Newsreader.woff2") +> export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) { +> // ... rest of implementation remains same +> const fonts: SatoriOptions["fonts"] = [ +> ...headerFontData.map((data, idx) => ({ +> name: headerFontName, +> data, +> weight: headerWeights[idx], +> style: "normal" as const, +> })), +> ...bodyFontData.map((data, idx) => ({ +> name: bodyFontName, +> data, +> weight: bodyWeights[idx], +> style: "normal" as const, +> })), +> { +> name: "Newsreader", +> data: await fs.promises.readFile(path.resolve(newsreaderFontPath)), +> weight: 400, +> style: "normal" as const, +> }, +> ] +> +> return fonts +> } +> ``` +> +> This font then can be used with your custom structure. + +## Examples + +Here are some example image components you can use as a starting point: + +### Basic Example + +This example will generate images that look as follows: + +| Light | Dark | +| ------------------------------------------ | ----------------------------------------- | +| ![[custom-social-image-preview-light.png]] | ![[custom-social-image-preview-dark.png]] | + +```tsx +import { SatoriOptions } from "satori/wasm" +import { GlobalConfiguration } from "../cfg" +import { SocialImageOptions, UserOpts } from "./imageHelper" +import { QuartzPluginData } from "../plugins/vfile" + +export const customImage: SocialImageOptions["imageStructure"] = ( + cfg: GlobalConfiguration, + userOpts: UserOpts, + title: string, + description: string, + fonts: SatoriOptions["fonts"], + fileData: QuartzPluginData, +) => { + // How many characters are allowed before switching to smaller font + const fontBreakPoint = 22 + const useSmallerFont = title.length > fontBreakPoint + + const { colorScheme } = userOpts + return ( +
+
+

+ {title} +

+

+ {description} +

+
+
+
+ ) +} +``` + +### Advanced Example + +The following example includes a customized social image with a custom background and formatted date: + +```typescript title="custom-og.tsx" +export const og: SocialImageOptions["Component"] = ( + cfg: GlobalConfiguration, + fileData: QuartzPluginData, + { colorScheme }: Options, + title: string, + description: string, + fonts: SatoriOptions["fonts"], +) => { + let created: string | undefined + let reading: string | undefined + if (fileData.dates) { + created = formatDate(getDate(cfg, fileData)!, cfg.locale) + } + const { minutes, text: _timeTaken, words: _words } = readingTime(fileData.text!) + reading = i18n(cfg.locale).components.contentMeta.readingTime({ + minutes: Math.ceil(minutes), + }) + + const Li = [created, reading] + + return ( +
+
+
+ +
+

+ {title} +

+
    + {Li.map((item, index) => { + if (item) { + return
  • {item}
  • + } + })} +
+
+

+ {description} +

+
+
+ ) +} +``` + +## API + +- Category: Emitter +- Function name: `ExternalPlugin.CustomOgImages()`. +- Source: [`quartz-community/og-image`](https://github.com/quartz-community/og-image) +- Install: `npx quartz plugin add github:quartz-community/og-image` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Darkmode.md b/Local/storage/thlab-notes/worker/docs/plugins/Darkmode.md new file mode 100644 index 0000000..9cf97fc --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Darkmode.md @@ -0,0 +1,37 @@ +--- +title: Darkmode +description: Toggle between light and dark themes. +tags: + - plugin/component +image: "#0052cc" +repository: "[quartz-community/darkmode](https://github.com/quartz-community/darkmode)" +enabled: true +required: false +--- + +Dark mode toggle. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[plugins/Darkmode]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `enabled`: Whether to enable the dark mode toggle. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/darkmode + enabled: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Darkmode()`. +- Source: [`quartz-community/darkmode`](https://github.com/quartz-community/darkmode) +- Install: `npx quartz plugin add github:quartz-community/darkmode` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Description.md b/Local/storage/thlab-notes/worker/docs/plugins/Description.md new file mode 100644 index 0000000..dd04632 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Description.md @@ -0,0 +1,30 @@ +--- +title: Description +description: Generates page descriptions for metadata and previews. +tags: + - plugin/transformer +image: +repository: "[quartz-community/description](https://github.com/quartz-community/description)" +enabled: true +required: false +--- + +This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing. + +If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `descriptionLength`: the target length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length. +- `maxDescriptionLength`: the hard maximum length of the description. If the generated description exceeds this, it is truncated with an ellipsis. Default is 300 characters. +- `replaceExternalLinks`: If `true` (default), replace external links with their domain and path in the description (e.g. `https://domain.tld/some_page/another_page?query=hello&target=world` is replaced with `domain.tld/some_page/another_page`). + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.Description()`. +- Source: [`quartz-community/description`](https://github.com/quartz-community/description) +- Install: `npx quartz plugin add github:quartz-community/description` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages Demo.md b/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages Demo.md new file mode 100644 index 0000000..d6bc44d --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages Demo.md @@ -0,0 +1,28 @@ +--- +title: Encrypted Pages Demo +password: quartz +unlisted: true +tags: + - plugin/transformer +image: +--- + +Congratulations! You've successfully decrypted this page. 🎉 + +This is a live demo of the [[EncryptedPages]] plugin. The content you're reading was encrypted at build time using AES-256-GCM and decrypted in your browser using the Web Crypto API. This page is also `unlisted: true`, which means it was hidden from every discovery surface on the site until you entered the password. + +## What just happened? + +1. At build time, the plugin read the `password` field from this page's frontmatter and encrypted all content below the title. +2. Because this page is `unlisted: true`, the plugin emitted its metadata (slug, title, links, tags) to a separate `static/encryptedContentIndex.json` file, encrypted with this page's own password. +3. When you visited this page, you were shown a password prompt instead of the content. The page was absent from the sidebar graph, explorer, search, RSS, sitemap, backlinks, tag listings, and bases views. +4. After entering the correct password, the plugin derived an encryption key using PBKDF2 and decrypted the content client-side. +5. The plugin then used the cached password to unlock this page's entry in the shadow content index and patched the in-memory content index in place. A `content-index-updated` event was dispatched, so graph, explorer, and search re-initialized with the newly unlocked entry — if you navigate back to any other page now, you will see this page in the sidebar, the graph, and search results. Server-side rendered listings (backlinks, recent notes, tag pages, folder listings, and [[BasesPage|bases views]]) were baked into HTML at build time and will not update within this session; they will only reflect decrypted pages on a fresh build of the site. + +## Password caching + +Your password has been cached in session storage. If there were other encrypted pages on this site with the same password, the plugin would automatically try this password on each one — unlocking its content as well as its entry in the shadow content index — so you'd only need to enter it once per session. + +## Try it yourself + +To add encrypted pages to your own Quartz site, install the plugin and add a `password` field to any page's frontmatter. See [[EncryptedPages]] for full setup instructions. diff --git a/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages.md b/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages.md new file mode 100644 index 0000000..557c0d5 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/EncryptedPages.md @@ -0,0 +1,129 @@ +--- +title: EncryptedPages +description: Password-protected encrypted pages with shadow content index. +tags: + - plugin/transformer + - plugin/emitter +image: "#FF1493" +new-in-v5: true +repository: "[quartz-community/encrypted-pages](https://github.com/quartz-community/encrypted-pages)" +enabled: true +required: false +--- + +Password-protected encrypted pages. Encrypts page content at build time using AES-256-GCM and decrypts client-side with the Web Crypto API. Passwords are set per-page via frontmatter. A companion emitter writes an encrypted shadow content index so unlisted encrypted pages can be dynamically revealed in graph, explorer, and search after a successful decryption — without ever leaking their metadata to visitors who do not hold the password. + +> [!example] Live demo +> Try it yourself: [[EncryptedPages Demo]]. The password is `quartz`. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Usage + +Add a `password` field to any page's frontmatter to encrypt it: + +```yaml +--- +title: My Secret Page +password: mysecretpassword +--- +``` + +The page content will be encrypted at build time. Visitors must enter the correct password to view the content. + +Successful passwords are cached in the browser's session storage and automatically tried on other encrypted pages for convenience. + +### Hiding encrypted pages from discovery surfaces + +By default, encrypted pages still appear in the graph, explorer, search, RSS, sitemap, and backlinks — visitors can see the page exists and its title, but cannot read the content without the password. + +To hide an encrypted page entirely until a visitor successfully decrypts it, set `unlisted: true` in its frontmatter: + +```yaml +--- +title: My Secret Page +password: mysecretpassword +unlisted: true +--- +``` + +An unlisted page: + +- Is **absent** from `contentIndex.json`, `sitemap.xml`, the RSS feed, backlinks, recent notes, folder listings, tag listings, bases views, graph, explorer, and search. +- Is still emitted as HTML, so it remains accessible by direct URL. +- Has its metadata (slug, title, links, tags) written to a separate `static/encryptedContentIndex.json` file, encrypted with the page's own password. +- Is dynamically re-added to the in-memory content index when a visitor successfully decrypts it, so graph, explorer, and search reflect it for the rest of the browser session. Server-rendered listings — backlinks, recent notes, tag pages, folder listings, and [[BasesPage|bases views]] — remain statically hidden even after decryption because they are baked as HTML at build time. + +To make this the default for every encrypted page on your site, set `unlistWhenEncrypted: true` in the plugin options. Individual pages can then opt back in with `unlisted: false`. + +> [!note] +> The `unlisted: true` frontmatter field above only takes effect for encrypted pages when this plugin is installed. If you also want `unlisted: true` to work on **non-encrypted** pages across your site, install [[UnlistedPages]] alongside this one. The two plugins compose cleanly — when both are enabled, `unlisted: true` hides any page, encrypted or not, from every discovery surface that respects the `file.data.unlisted` convention. + +### Permanently hiding encrypted pages (`stealth`) + +By default, an `unlisted: true` encrypted page is _revealed_ in graph, explorer, and search after a visitor successfully decrypts it. This is usually what you want: the user just proved they know the password, so showing them the page in the sidebar makes sense for the rest of their session. + +If you instead want a page that stays permanently invisible — accessible only by direct URL, even to users who have successfully decrypted other pages on the same site — set `stealth: true` in its frontmatter: + +```yaml +--- +title: Deep Secret +password: mysecretpassword +stealth: true +--- +``` + +A stealth page: + +- Is **absent** from every discovery surface, same as any `unlisted` page. +- Has **no entry** in the shadow content index (`encryptedContentIndex.json`). The plugin deliberately skips stealth pages when building the shadow index. +- Stays hidden even after the visitor enters the correct password. Since there is no shadow-index entry to decrypt, there is nothing to patch into the in-memory content index — graph, explorer, and search never learn the page exists. Only the decrypted HTML is visible to the user on the page itself. +- The password is still cached in session storage, so re-visiting the same stealth page will auto-unlock it. + +`stealth: true` implies `unlisted: true` — you do not need to set both, and if you write `stealth: true, unlisted: false` the stealth flag wins. On non-encrypted pages `stealth: true` has no effect (there is no shadow index to skip). + +Use stealth pages for "secret door" content that should only reach users who already know the exact URL: private notes linked from an external wiki, personal pages you send to specific people, or anything you never want to show up in a site-internal search even to authenticated readers. + +## Configuration + +This plugin provides a transformer, an emitter, and a component. All options are set on a single config entry and shared between the transformer and the emitter — Quartz instantiates both automatically. + +- `iterations`: PBKDF2 iteration count for key derivation. Higher values are more secure but slower to unlock. Defaults to `600000`. +- `passwordField`: Frontmatter field name that holds the page password. Shared by the transformer and the emitter. Defaults to `"password"`. +- `unlistWhenEncrypted`: If `true`, every encrypted page is marked `unlisted` unless its frontmatter explicitly overrides it. Defaults to `false`. +- `outputPath`: Output path for the shadow content index, relative to Quartz's output directory. Defaults to `"static/encryptedContentIndex.json"`. + +### Component options + +- `className`: CSS class for the component wrapper. Defaults to `"encrypted-page-wrapper"`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json +``` + +> [!warning] +> The `EncryptedPages` transformer replaces the entire HAST tree of an encrypted page with an opaque ciphertext container. Any transformer that needs to read the real HTML — in particular [[CrawlLinks]], which populates the links used by backlinks and the shadow content index — must run **before** `EncryptedPages`. Use the `order` field in `quartz.config.yaml` to control this. + +## Security + +- Content is encrypted with AES-256-GCM using PBKDF2 SHA-256 key derivation. +- Plaintext is stripped from search indices, RSS feeds, and the shadow content index regardless of visibility setting. +- The shadow content index is a flat array of opaque encrypted blobs. An attacker who downloads it learns only the number of unlisted encrypted pages and the PBKDF2 iteration count — no slugs, titles, or link relationships leak. +- Passwords are set per-page in frontmatter. Avoid committing passwords to public repositories. +- This is client-side encryption of a static site. It protects against casual browsing but not against determined attackers with access to the page source. + +## API + +- Category: Transformer, Emitter +- Function name: `ExternalPlugin.EncryptedPages()`, `ExternalPlugin.EncryptedContentIndex()`. +- Source: [`quartz-community/encrypted-pages`](https://github.com/quartz-community/encrypted-pages) +- Install: `npx quartz plugin add github:quartz-community/encrypted-pages` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ExplicitPublish.md b/Local/storage/thlab-notes/worker/docs/plugins/ExplicitPublish.md new file mode 100644 index 0000000..61da193 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ExplicitPublish.md @@ -0,0 +1,24 @@ +--- +title: ExplicitPublish +description: "Only publishes pages explicitly marked with publish: true." +tags: + - plugin/filter +image: +repository: "[quartz-community/explicit-publish](https://github.com/quartz-community/explicit-publish)" +enabled: false +required: false +--- + +This plugin filters content based on an explicit `publish` flag in the frontmatter, allowing only content that is explicitly marked for publication to pass through. It's the opt-in version of [[RemoveDrafts]]. See [[private pages]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Filter +- Function name: `ExternalPlugin.ExplicitPublish()`. +- Source: [`quartz-community/explicit-publish`](https://github.com/quartz-community/explicit-publish) +- Install: `npx quartz plugin add github:quartz-community/explicit-publish` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Explorer.md b/Local/storage/thlab-notes/worker/docs/plugins/Explorer.md new file mode 100644 index 0000000..22c8eec --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Explorer.md @@ -0,0 +1,70 @@ +--- +title: Explorer +description: File tree explorer sidebar. +tags: + - plugin/component +image: +repository: "[quartz-community/explorer](https://github.com/quartz-community/explorer)" +enabled: true +required: false +--- + +File tree explorer sidebar. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[plugins/Explorer]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +**YAML options** (in `quartz.config.yaml`): + +- `title`: The title of the explorer. Defaults to `Explorer`. +- `folderClickBehavior`: The behavior when a folder is clicked. Can be `"link"` to navigate or `"collapse"` to toggle. Defaults to `link`. +- `folderDefaultState`: The default state of folders. Can be `"collapsed"` or `"open"`. Defaults to `collapsed`. +- `useSavedState`: Whether to use local storage to save the state of the explorer. Defaults to `true`. + +**TS override options** (in `quartz.ts`, for callback functions that can't be expressed in YAML): + +- `sortFn`: Custom sort function for ordering files and folders. +- `filterFn`: Custom filter function to exclude specific nodes. +- `mapFn`: Custom map function to transform node properties (e.g. display names). +- `order`: Array controlling the order of operations. Defaults to `["filter", "map", "sort"]`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/explorer + enabled: true + options: + title: Explorer + folderClickBehavior: link + folderDefaultState: collapsed + useSavedState: true +``` + +### TS override example + +```ts title="quartz.ts" +import * as ExternalPlugin from "./.quartz/plugins" + +// Must be placed before loadQuartzConfig() +ExternalPlugin.Explorer({ + mapFn: (node) => { + node.displayName = node.displayName.toUpperCase() + return node + }, +}) +``` + +See [[features/explorer#Advanced customization]] for more examples. + +## API + +- Category: Component +- Function name: `ExternalPlugin.Explorer()`. +- Source: [`quartz-community/explorer`](https://github.com/quartz-community/explorer) +- Install: `npx quartz plugin add github:quartz-community/explorer` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Favicon.md b/Local/storage/thlab-notes/worker/docs/plugins/Favicon.md new file mode 100644 index 0000000..4842b99 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Favicon.md @@ -0,0 +1,25 @@ +--- +title: Favicon +description: Emits the site favicon. +tags: + - plugin/emitter +image: +repository: "[quartz-community/favicon](https://github.com/quartz-community/favicon)" +enabled: true +required: false +--- + +This plugin emits a `favicon.ico` into the `public` folder. It creates the favicon from `icon.png` located in the `quartz/static` folder. +The plugin resizes `icon.png` to 48x48px to make it as small as possible. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `ExternalPlugin.Favicon()`. +- Source: [`quartz-community/favicon`](https://github.com/quartz-community/favicon) +- Install: `npx quartz plugin add github:quartz-community/favicon` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/FolderPage.md b/Local/storage/thlab-notes/worker/docs/plugins/FolderPage.md new file mode 100644 index 0000000..1b8ee6b --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/FolderPage.md @@ -0,0 +1,31 @@ +--- +title: FolderPage +description: Generates listing pages for folders. +tags: + - plugin/pageType +image: +repository: "[quartz-community/folder-page](https://github.com/quartz-community/folder-page)" +enabled: true +required: false +--- + +This plugin is a page type plugin that generates index pages for folders, creating a listing page for each folder that contains multiple content files. It uses the `default` [[layout#Page Frames|page frame]] (three-column layout with sidebars). See [[folder and tag listings]] for more information. + +Example: [[advanced/|Advanced]] + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `showFolderCount`: Whether to display the number of pages in the folder. Defaults to `true`. +- `showSubfolders`: Whether to include pages from subfolders in the listing. Defaults to `true`. +- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order. Requires a TS override. +- `prefixFolders`: If `true`, generated folder page titles are prefixed with "Folder: " (e.g. "Folder: notes"). Defaults to `false`. + +## API + +- Category: Page Type +- Function name: `ExternalPlugin.FolderPage()`. +- Source: [`quartz-community/folder-page`](https://github.com/quartz-community/folder-page) +- Install: `npx quartz plugin add github:quartz-community/folder-page` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Fonts.md b/Local/storage/thlab-notes/worker/docs/plugins/Fonts.md new file mode 100644 index 0000000..f12d2f6 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Fonts.md @@ -0,0 +1,226 @@ +--- +title: Fonts +description: Fine-grained font control with per-heading support, Google Fonts integration, and theme font discovery. +tags: + - plugin/transformer + - plugin/emitter +image: +repository: "[quartz-community/fonts](https://github.com/quartz-community/fonts)" +enabled: true +required: false +--- + +This plugin provides fine-grained control over fonts in your Quartz site. It supports per-heading font families, automatic theme font discovery when used with [Quartz Themes](https://github.com/saberzero1/quartz-themes), Google Fonts integration with automatic weight and italic loading, and falls back to Obsidian's default system font stacks. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Why use Fonts? + +Quartz uses three CSS variables for fonts: `--headerFont`, `--bodyFont`, and `--codeFont`. Obsidian themes use a different system with per-heading variables (`--h1-font` through `--h6-font`), `--font-text`, and `--font-monospace`. These two systems don't bridge correctly, causing heading fonts to not render as themes intend. + +Fonts solves this by: + +1. Bridging the Obsidian and Quartz font systems +2. Emitting **unlayered** CSS that correctly overrides Quartz's base heading styles +3. Providing per-heading font control that neither system offers alone +4. Optionally loading fonts from Google Fonts with fine-grained weight and italic control + +## Configuration + +Font options accept either a CSS font-family string or an object with Google Fonts loading control: + +```yaml +# String form +body: '"Inter", sans-serif' + +# Object form (for Google Fonts weight/italic control) +body: + name: Inter + weights: [400, 600, 700] + includeItalic: true +``` + +This plugin accepts the following configuration options: + +| Option | Type | Default | Description | +| --------------- | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `title` | `FontSpecification` | `header` value | Font family for the site title. | +| `body` | `FontSpecification` | Obsidian default | Font family for body text. | +| `header` | `FontSpecification` | Obsidian default | Default font family for all headings (h1-h6). | +| `code` | `FontSpecification` | Obsidian default | Font family for code and monospace elements. | +| `interface` | `FontSpecification` | Obsidian default | Font family for UI elements. | +| `h1` – `h6` | `FontSpecification` | `header` value | Per-heading font family overrides. | +| `useThemeFonts` | `boolean` | `true` | Use fonts from [Quartz Themes](https://github.com/saberzero1/quartz-themes) as defaults when it is installed. | +| `fontOrigin` | `string` | `"googleFonts"` | `"googleFonts"` to load from Google Fonts CDN, `"selfHosted"` to download and serve locally, `"local"` for no loading. | + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/fonts + enabled: true + options: + useThemeFonts: true + fontOrigin: googleFonts +``` + +### Font resolution + +Fonts are resolved using a priority chain: + +``` +User config (plugin options) + → Theme fonts (from Quartz Themes, if installed) + → Obsidian defaults (system font stacks) +``` + +For individual headings: + +``` +h1 option → header option → theme --h1-font → theme font → Obsidian default +``` + +For the site title: + +``` +title option → header option → theme font → Obsidian default +``` + +## Usage with Quartz Themes + +When [Quartz Themes](https://github.com/saberzero1/quartz-themes) is installed and enabled, Fonts automatically discovers the theme's font metadata and uses it as defaults. Any options you explicitly set in Fonts will override the theme fonts. + +Fonts must run **after** Quartz Themes. This is handled automatically by plugin ordering (Quartz Themes = 10, Fonts = 60). + +> [!warning] +> If Quartz Themes is enabled but hasn't run yet when Fonts executes, you'll see a warning in the console. Make sure Quartz Themes has a lower `defaultOrder` than Fonts. + +## Usage without Quartz Themes + +Fonts works standalone. Without a theme, it falls back to Obsidian's default system font stacks. You can set fonts explicitly via the plugin options. + +## Examples + +```yaml title="quartz.config.yaml" +# Use theme fonts automatically (default behavior) +- source: github:quartz-community/fonts + enabled: true + +# Override just the heading font +- source: github:quartz-community/fonts + enabled: true + options: + header: '"Playfair Display", serif' + +# Full control with per-heading fonts +- source: github:quartz-community/fonts + enabled: true + options: + body: '"Inter", sans-serif' + header: '"Playfair Display", serif' + code: '"JetBrains Mono", monospace' + h1: '"Playfair Display", serif' + h2: '"Lora", serif' + +# Load from Google Fonts automatically +- source: github:quartz-community/fonts + enabled: true + options: + fontOrigin: googleFonts + body: Inter + header: Playfair Display + code: JetBrains Mono + +# Google Fonts with weight/italic control +- source: github:quartz-community/fonts + enabled: true + options: + fontOrigin: googleFonts + body: + name: Inter + weights: [400, 600, 700] + includeItalic: true + header: + name: Playfair Display + weights: [400, 700] + code: + name: JetBrains Mono + weights: [400] + +# Custom title font (separate from header) +- source: github:quartz-community/fonts + enabled: true + options: + fontOrigin: googleFonts + title: Abril Fatface + header: Playfair Display + body: Inter + code: JetBrains Mono + +# Self-hosted fonts (downloaded at build time, no external requests) +- source: github:quartz-community/fonts + enabled: true + options: + fontOrigin: selfHosted + body: Inter + header: Playfair Display + code: JetBrains Mono + +# Ignore theme fonts entirely +- source: github:quartz-community/fonts + enabled: true + options: + useThemeFonts: false + body: '"Inter", sans-serif' +``` + +## Self-Hosted Fonts + +When `fontOrigin: selfHosted` is set, Fonts downloads fonts from Google Fonts during the build and serves them from your site's `static/fonts/` directory. This makes your site fully self-contained with no external requests to Google at runtime. + +At build time, the plugin: + +1. Fetches the Google Fonts CSS for your configured fonts +2. Downloads each font file (`.woff2`, `.woff`, etc.) +3. Writes the font files to `static/fonts/` in your build output +4. Generates a `quartz-fonts.css` file with `@font-face` rules pointing to the local files + +> [!note] +> Self-hosted fonts require `baseUrl` to be set in your Quartz configuration, since font URLs in the CSS need an absolute path. + +```yaml title="quartz.config.yaml" +configuration: + baseUrl: "example.com" + +plugins: + - source: github:quartz-community/fonts + enabled: true + options: + fontOrigin: selfHosted + body: Inter + header: Playfair Display + code: JetBrains Mono +``` + +## Google Fonts Validation + +When `fontOrigin: googleFonts` is set and the optional [`google-font-metadata`](https://www.npmjs.com/package/google-font-metadata) package is installed, Fonts validates your font configuration at build time: + +- Checks that font family names exist in Google Fonts. +- Warns if requested weights are not available for a font. +- Warns if italic is requested but the font doesn't support it. + +Install it to enable validation: + +```bash +npm install google-font-metadata +``` + +Validation warnings are logged to the console but do not block the build. + +## API + +- Category: Transformer, Emitter +- Function name: `ExternalPlugin.Fonts()` (transformer), `ExternalPlugin.FontsEmitter()` (emitter). +- Source: [`quartz-community/fonts`](https://github.com/quartz-community/fonts) +- Install: `npx quartz plugin add github:quartz-community/fonts` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Footer.md b/Local/storage/thlab-notes/worker/docs/plugins/Footer.md new file mode 100644 index 0000000..fb09dc0 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Footer.md @@ -0,0 +1,39 @@ +--- +title: Footer +description: Page footer with configurable links. +tags: + - plugin/component +image: +repository: "[quartz-community/footer](https://github.com/quartz-community/footer)" +enabled: true +required: false +--- + +This plugin renders a footer at the bottom of the page with a "Created with Quartz" message and a set of configurable links. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Configuration + +This plugin accepts the following configuration options: + +- `links`: A map of link labels to their URLs to display in the footer. Defaults to `{}`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Footer()`. +- Source: [`quartz-community/footer`](https://github.com/quartz-community/footer) +- Install: `npx quartz plugin add github:quartz-community/footer` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Frontmatter.md b/Local/storage/thlab-notes/worker/docs/plugins/Frontmatter.md new file mode 100644 index 0000000..4919614 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Frontmatter.md @@ -0,0 +1,111 @@ +--- +title: Frontmatter +aliases: + - note-properties + - Note Properties +description: Parses frontmatter and displays note properties in a collapsible panel. +tags: + - plugin/transformer + - plugin/component +publish: true +enableToc: true +image: +repository: "[quartz-community/note-properties](https://github.com/quartz-community/note-properties)" +enabled: true +required: true +--- + +This plugin parses the frontmatter of the page using the [gray-matter](https://github.com/jonschlinkert/gray-matter) library and optionally displays selected properties in a collapsible panel. See [[authoring content#Syntax]], [[Obsidian compatibility]] and [[OxHugo compatibility]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +> [!warning] +> This plugin must not be removed, otherwise Quartz will break. + +## Configuration + +This plugin accepts the following configuration options: + +- `delimiters`: the delimiters to use for the frontmatter. Can have one value (e.g. `"---"`) or separate values for opening and closing delimiters (e.g. `["---", "~~~"]`). Defaults to `"---"`. +- `language`: the language to use for parsing the frontmatter. Can be `yaml` (default) or `toml`. +- `includeAll`: include all frontmatter properties in the properties panel. When `false`, only `includedProperties` are shown. Defaults to `false`. +- `includedProperties`: properties to include when `includeAll` is `false`. Defaults to `["description", "tags", "aliases"]`. +- `excludedProperties`: properties to always exclude from display, even when `includeAll` is `true`. Defaults to `[]`. +- `hidePropertiesView`: hide the visual properties panel while still processing frontmatter. Useful if you only need frontmatter parsing without the UI. Defaults to `false`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: "---" + language: yaml +``` + +## Properties panel + +When enabled, this plugin renders a collapsible "Properties" panel before the page body. The panel displays selected frontmatter fields in a table with automatic type rendering: + +- **Strings** are shown as plain text. [[wikilinks]] and [markdown links](https://example.com) within strings are rendered as clickable links. Wikilink targets are slugified the same way as body-content links (e.g. `[[My Note]]` resolves to `my-note`) and matching is case-insensitive to mirror Obsidian's behavior, so `[[MyNote]]`, `[[mynote]]`, and `[[MYNOTE]]` all point to the same page. +- **Arrays** are rendered as comma-separated lists. +- **Booleans** are rendered as disabled checkboxes. +- **Numbers** are rendered in a monospace font. +- **Objects** are rendered as JSON in a code block. +- **Tags** get special treatment: they are rendered as highlighted links that point to the corresponding tag page. +- **Null/undefined** values are shown as an em-dash (—). + +### Per-note overrides + +You can control the properties panel on a per-note basis using frontmatter keys: + +- `quartz-properties` (or `quartzProperties`): set to `true` to force-show the panel, or `false` to force-hide it, overriding the global `hidePropertiesView` setting. +- `quartz-properties-collapse` (or `quartzPropertiesCollapse`): set to `true` to start the panel collapsed, or `false` to start it expanded, overriding the default collapse state. + +These keys are automatically excluded from the visible properties table. + +```yaml title="Example frontmatter" +--- +title: My Note +quartz-properties: true +quartz-properties-collapse: false +--- +``` + +## Supported frontmatter + +Quartz supports the following frontmatter fields. Where multiple keys are listed, they are aliases — the first matching key is used. + +| Field | Keys | Description | +| ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | `title` | Page title. Falls back to filename if empty. | +| Description | `description` | Page description for metadata and search. | +| Tags | `tags`, `tag` | Categorization tags. Slugified the same way as file paths: spaces become `-`, `&` becomes `-and-`, `%` becomes `-percent`, and tags are lowercased so that `#MyTag` and `#mytag` resolve to the same tag page (matching Obsidian). | +| Aliases | `aliases`, `alias` | Alternative names for the page, used for link resolution. | +| Permalink | `permalink` | Custom URL slug. Also added to aliases. | +| CSS classes | `cssclasses`, `cssclass` | CSS classes applied to the page body. | +| Social image | `socialImage`, `image`, `cover` | Image used for social media previews. | +| Social description | `socialDescription` | Description used specifically for social media previews. | +| Created date | `created`, `date` | When the note was created. | +| Modified date | `modified`, `lastmod`, `updated`, `last-modified` | When the note was last modified. Falls back to `created` if unset. | +| Published date | `published`, `publishDate`, `date` | When the note was published. | +| Publish | `publish` | Whether the note should be published. | +| Draft | `draft` | Whether the note is a draft. | +| Comments | `comments` | Whether comments are enabled for the note. | +| Language | `lang` | Language code for the note. | +| Enable TOC | `enableToc` | Whether to show the table of contents. | + +## API + +- Category: Transformer, Component +- Function name: `ExternalPlugin.NoteProperties()`. +- Source: [`quartz-community/note-properties`](https://github.com/quartz-community/note-properties) +- Install: `npx quartz plugin add github:quartz-community/note-properties` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/GitHubFlavoredMarkdown.md b/Local/storage/thlab-notes/worker/docs/plugins/GitHubFlavoredMarkdown.md new file mode 100644 index 0000000..d1fe89e --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/GitHubFlavoredMarkdown.md @@ -0,0 +1,29 @@ +--- +title: GitHubFlavoredMarkdown +description: GitHub Flavored Markdown support (tables, task lists, footnotes, strikethrough). +tags: + - plugin/transformer +image: +repository: "[quartz-community/github-flavored-markdown](https://github.com/quartz-community/github-flavored-markdown)" +enabled: true +required: false +--- + +This plugin enhances Markdown processing to support GitHub Flavored Markdown (GFM) which adds features like autolink literals, footnotes, strikethrough, tables and tasklists. + +In addition, this plugin adds optional features for typographic refinement (such as converting straight quotes to curly quotes, dashes to en-dashes/em-dashes, and ellipses) and automatic heading links as a symbol that appears next to the heading on hover. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `enableSmartyPants`: When true, enables typographic enhancements. Default is true. +- `linkHeadings`: When true, automatically adds links to headings. Default is true. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.GitHubFlavoredMarkdown()`. +- Source: [`quartz-community/github-flavored-markdown`](https://github.com/quartz-community/github-flavored-markdown) +- Install: `npx quartz plugin add github:quartz-community/github-flavored-markdown` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Graph.md b/Local/storage/thlab-notes/worker/docs/plugins/Graph.md new file mode 100644 index 0000000..177d632 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Graph.md @@ -0,0 +1,83 @@ +--- +title: Graph +description: Interactive link graph visualization. +tags: + - plugin/component +image: +repository: "[quartz-community/graph](https://github.com/quartz-community/graph)" +enabled: true +required: false +--- + +Interactive graph visualization. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[graph view]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `localGraph`: Options for the local graph view. +- `globalGraph`: Options for the global graph view. + +Both `localGraph` and `globalGraph` accept the following options: + +- `drag`: Enable dragging nodes. Defaults to `true`. +- `zoom`: Enable zooming. Defaults to `true`. +- `depth`: The depth of the graph. Defaults to `1` for local and `-1` for global. +- `scale`: The initial scale of the graph. Defaults to `1.1` for local and `0.9` for global. +- `repelForce`: The force that pushes nodes apart. Defaults to `0.5`. +- `centerForce`: The force that pulls nodes to the center. Defaults to `0.3` for local and `0.2` for global. +- `linkDistance`: The distance between linked nodes. Defaults to `30`. +- `fontSize`: The font size of node labels. Defaults to `0.6`. +- `opacityScale`: The scale of node opacity. Defaults to `1`. +- `removeTags`: Tags to exclude from the graph. Defaults to `[]`. +- `showTags`: Whether to show tags in the graph. Defaults to `true`. +- `enableRadial`: Whether to enable radial layout. Defaults to `false` for local and `true` for global. +- `focusOnHover`: Whether to focus on the hovered node. Defaults to `false` for local and `true` for global. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/graph + enabled: true + options: + localGraph: + drag: true + zoom: true + depth: 1 + scale: 1.1 + repelForce: 0.5 + centerForce: 0.3 + linkDistance: 30 + fontSize: 0.6 + opacityScale: 1 + removeTags: [] + showTags: true + focusOnHover: false + enableRadial: false + globalGraph: + drag: true + zoom: true + depth: -1 + scale: 0.9 + repelForce: 0.5 + centerForce: 0.3 + linkDistance: 30 + fontSize: 0.6 + opacityScale: 1 + removeTags: [] + showTags: true + focusOnHover: true + enableRadial: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Graph()`. +- Source: [`quartz-community/graph`](https://github.com/quartz-community/graph) +- Install: `npx quartz plugin add github:quartz-community/graph` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/HardLineBreaks.md b/Local/storage/thlab-notes/worker/docs/plugins/HardLineBreaks.md new file mode 100644 index 0000000..37a0917 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/HardLineBreaks.md @@ -0,0 +1,24 @@ +--- +title: HardLineBreaks +description: Treats single newlines as hard line breaks. +tags: + - plugin/transformer +image: "#ff8000" +repository: "[quartz-community/hard-line-breaks](https://github.com/quartz-community/hard-line-breaks)" +enabled: false +required: false +--- + +This plugin automatically converts single line breaks in Markdown text into hard line breaks in the HTML output. This plugin is not enabled by default as this doesn't follow the semantics of actual Markdown but you may enable it if you'd like parity with [[Obsidian compatibility|Obsidian]]. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.HardLineBreaks()`. +- Source: [`quartz-community/hard-line-breaks`](https://github.com/quartz-community/hard-line-breaks) +- Install: `npx quartz plugin add github:quartz-community/hard-line-breaks` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Latex.md b/Local/storage/thlab-notes/worker/docs/plugins/Latex.md new file mode 100644 index 0000000..08fa50c --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Latex.md @@ -0,0 +1,30 @@ +--- +title: Latex +description: Renders LaTeX math expressions via KaTeX or Typst. +tags: + - plugin/transformer +image: +repository: "[quartz-community/latex](https://github.com/quartz-community/latex)" +enabled: true +required: false +--- + +This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/), `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html), or `"typst"` for [Typst](https://typst.app/) (a new way to compose LaTeX equation). Defaults to KaTeX. +- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}` +- `katexOptions`: Additional options passed to the KaTeX renderer. See the [KaTeX docs](https://katex.org/docs/options) for available options. +- `mathJaxOptions`: Additional options passed to the MathJax renderer. See the [MathJax docs](https://docs.mathjax.org/en/latest/options/) for available options. +- `typstOptions`: Additional options passed to the Typst renderer. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.Latex()`. +- Source: [`quartz-community/latex`](https://github.com/quartz-community/latex) +- Install: `npx quartz plugin add github:quartz-community/latex` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/NotFoundPage.md b/Local/storage/thlab-notes/worker/docs/plugins/NotFoundPage.md new file mode 100644 index 0000000..bb49697 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/NotFoundPage.md @@ -0,0 +1,19 @@ +--- +title: NotFoundPage +tags: + - plugin/pageType +image: +--- + +This plugin emits a 404 (Not Found) page for broken or non-existent URLs. It uses the `minimal` [[layout#Page Frames|page frame]] (no sidebars, no header or beforeBody chrome — only content and footer) to present a clean error page. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Page Type +- Function name: `Plugin.NotFoundPage()` (internal plugin). +- Source: [`quartz/plugins/pageTypes/404.ts`](https://github.com/jackyzha0/quartz/blob/v5/quartz/plugins/pageTypes/404.ts) diff --git a/Local/storage/thlab-notes/worker/docs/plugins/NoteProperties.md b/Local/storage/thlab-notes/worker/docs/plugins/NoteProperties.md new file mode 100644 index 0000000..3e40ebf --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/NoteProperties.md @@ -0,0 +1,23 @@ +--- +title: NoteProperties +description: Displays frontmatter properties in a collapsible panel. +tags: + - plugin/component +image: +new-in-v5: true +repository: "[quartz-community/note-properties](https://github.com/quartz-community/note-properties)" +enabled: true +required: true +--- + +The NoteProperties plugin is documented under [[Frontmatter]]. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## API + +- Category: Component +- Function name: `ExternalPlugin.NoteProperties()`. +- Source: [`quartz-community/note-properties`](https://github.com/quartz-community/note-properties) +- Install: `npx quartz plugin add github:quartz-community/note-properties` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ObsidianFlavoredMarkdown.md b/Local/storage/thlab-notes/worker/docs/plugins/ObsidianFlavoredMarkdown.md new file mode 100644 index 0000000..7c99a54 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ObsidianFlavoredMarkdown.md @@ -0,0 +1,44 @@ +--- +title: ObsidianFlavoredMarkdown +description: Obsidian-specific Markdown extensions (wikilinks, callouts, highlights, tags, embeds). +tags: + - plugin/transformer +image: +repository: "[quartz-community/obsidian-flavored-markdown](https://github.com/quartz-community/obsidian-flavored-markdown)" +enabled: true +required: false +--- + +This plugin provides support for [[Obsidian compatibility]]. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `comments`: If `true` (default), enables parsing of `%%` style Obsidian comment blocks. +- `highlight`: If `true` (default), enables parsing of `==` style highlights within content. +- `wikilinks`:If `true` (default), turns [[wikilinks]] into regular links. +- `callouts`: If `true` (default), adds support for [[callouts|callout]] blocks for emphasizing content. +- `mermaid`: If `true` (default), enables [[Mermaid diagrams|Mermaid diagram]] rendering within Markdown files. +- `parseTags`: If `true` (default), parses and links tags within the content. +- `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks. +- `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`. +- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax. +- `enableTweetEmbed`: If `true` (default), enables the embedding of tweets as static blockquotes from Twitter/X URLs. +- `enableVideoEmbed`: If `true` (default), enables the embedding of video files. +- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content, including custom task characters (e.g. `- [?]`, `- [!]`, `- [/]`). Defaults to `false`. +- `enableObsidianUri`: If `true` (default), marks `obsidian://` protocol links with a CSS class and data attribute for custom styling. + +> [!note] +> The `disableBrokenWikilinks` option previously lived on this plugin. It has moved to [[CrawlLinks]], which owns link resolution and can honor the configured `markdownLinkResolution` strategy when deciding whether a link is broken. Users upgrading from earlier Quartz v5 betas should move the option from `ObsidianFlavoredMarkdown` to `CrawlLinks`. + +> [!warning] +> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content! + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.ObsidianFlavoredMarkdown()`. +- Source: [`quartz-community/obsidian-flavored-markdown`](https://github.com/quartz-community/obsidian-flavored-markdown) +- Install: `npx quartz plugin add github:quartz-community/obsidian-flavored-markdown` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/OxHugoFlavoredMarkdown.md b/Local/storage/thlab-notes/worker/docs/plugins/OxHugoFlavoredMarkdown.md new file mode 100644 index 0000000..e78da79 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/OxHugoFlavoredMarkdown.md @@ -0,0 +1,35 @@ +--- +title: OxHugoFlavoredMarkdown +description: Compatibility for ox-hugo exported Org-mode files. +tags: + - plugin/transformer +image: +repository: "[quartz-community/ox-hugo](https://github.com/quartz-community/ox-hugo)" +enabled: false +required: false +--- + +This plugin provides support for [ox-hugo](https://github.com/kaushalmodi/ox-hugo) compatibility. See [[OxHugo compatibility]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `wikilinks`: If `true` (default), converts Hugo `{{ relref }}` shortcodes to Quartz [[wikilinks]]. +- `removePredefinedAnchor`: If `true` (default), strips predefined anchors from headings. +- `removeHugoShortcode`: If `true` (default), removes Hugo shortcode syntax (`{{}}`) from the content. +- `replaceFigureWithMdImg`: If `true` (default), replaces `
` with `![]()`. +- `replaceOrgLatex`: If `true` (default), converts Org-mode [[features/Latex|Latex]] fragments to Quartz-compatible LaTeX wrapped in `$` (for inline) and `$$` (for block equations). + +> [!warning] +> While you can use this together with [[ObsidianFlavoredMarkdown]], it's not recommended because it might mutate the file in unexpected ways. Use with caution. +> +> If you use `toml` frontmatter, make sure to configure the [[Frontmatter]] plugin accordingly. See [[OxHugo compatibility]] for an example. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.OxHugoFlavoredMarkdown()`. +- Source: [`quartz-community/ox-hugo`](https://github.com/quartz-community/ox-hugo) +- Install: `npx quartz plugin add github:quartz-community/ox-hugo` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/PageTitle.md b/Local/storage/thlab-notes/worker/docs/plugins/PageTitle.md new file mode 100644 index 0000000..0fd78f0 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/PageTitle.md @@ -0,0 +1,24 @@ +--- +title: PageTitle +description: Renders the site title as a home link. +tags: + - plugin/component +image: +repository: "[quartz-community/page-title](https://github.com/quartz-community/page-title)" +enabled: true +required: false +--- + +This plugin renders the site-wide page title (configured via the `pageTitle` field in [[configuration]]) as a clickable link to the home page. It typically appears in the left sidebar. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. The displayed title is controlled by the `pageTitle` field in `quartz.config.yaml`. + +## API + +- Category: Component +- Function name: `ExternalPlugin.PageTitle()`. +- Source: [`quartz-community/page-title`](https://github.com/quartz-community/page-title) +- Install: `npx quartz plugin add github:quartz-community/page-title` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/ReaderMode.md b/Local/storage/thlab-notes/worker/docs/plugins/ReaderMode.md new file mode 100644 index 0000000..380f7e1 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/ReaderMode.md @@ -0,0 +1,38 @@ +--- +title: ReaderMode +description: Distraction-free reading mode toggle. +tags: + - plugin/component +image: +new-in-v5: true +repository: "[quartz-community/reader-mode](https://github.com/quartz-community/reader-mode)" +enabled: true +required: false +--- + +Distraction-free reading mode. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[reader mode]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `enabled`: Whether to enable reader mode. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/reader-mode + enabled: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.ReaderMode()`. +- Source: [`quartz-community/reader-mode`](https://github.com/quartz-community/reader-mode) +- Install: `npx quartz plugin add github:quartz-community/reader-mode` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/RecentNotes.md b/Local/storage/thlab-notes/worker/docs/plugins/RecentNotes.md new file mode 100644 index 0000000..5a1e7b4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/RecentNotes.md @@ -0,0 +1,47 @@ +--- +title: RecentNotes +description: Displays a list of recently modified notes. +tags: + - plugin/component +image: +repository: "[quartz-community/recent-notes](https://github.com/quartz-community/recent-notes)" +enabled: false +required: false +--- + +Shows recently modified notes. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[recent notes]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `title`: The title of the recent notes section. Defaults to `Recent notes`. +- `limit`: The maximum number of recent notes to display. Defaults to `3`. +- `showTags`: Whether to display the tags for each note. Defaults to `true`. +- `linkToMore`: A slug to a page that shows more notes. Defaults to `false`. +- `hideTagPages`: Whether to hide tag index pages from the list. Defaults to `false`. +- `hideFolderPages`: Whether to hide folder index pages from the list. Defaults to `false`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/recent-notes + enabled: true + options: + limit: 3 + showTags: true + hideTagPages: false + hideFolderPages: false +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.RecentNotes()`. +- Source: [`quartz-community/recent-notes`](https://github.com/quartz-community/recent-notes) +- Install: `npx quartz plugin add github:quartz-community/recent-notes` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/RemoveDrafts.md b/Local/storage/thlab-notes/worker/docs/plugins/RemoveDrafts.md new file mode 100644 index 0000000..77d4764 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/RemoveDrafts.md @@ -0,0 +1,24 @@ +--- +title: RemoveDrafts +description: Filters out pages marked as drafts. +tags: + - plugin/filter +image: +repository: "[quartz-community/remove-draft](https://github.com/quartz-community/remove-draft)" +enabled: true +required: false +--- + +This plugin filters out content from your vault, so that only finalized content is made available. This prevents [[private pages]] from being published. By default, it filters out all pages with `draft: true` in the frontmatter and leaves all other pages intact. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Filter +- Function name: `ExternalPlugin.RemoveDrafts()`. +- Source: [`quartz-community/remove-draft`](https://github.com/quartz-community/remove-draft) +- Install: `npx quartz plugin add github:quartz-community/remove-draft` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/RoamFlavoredMarkdown.md b/Local/storage/thlab-notes/worker/docs/plugins/RoamFlavoredMarkdown.md new file mode 100644 index 0000000..c08c0bc --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/RoamFlavoredMarkdown.md @@ -0,0 +1,34 @@ +--- +title: RoamFlavoredMarkdown +description: Compatibility for Roam Research export format. +tags: + - plugin/transformer +image: +repository: "[quartz-community/roam](https://github.com/quartz-community/roam)" +enabled: false +required: false +--- + +This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research compatibility]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options. +- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes. +- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes. +- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video. +- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio. +- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer. +- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes. +- `tableComponent`: If `true` (default), converts Roam table syntax into HTML tables. +- `attributeComponent`: If `true` (default), converts Roam attribute syntax into rendered attributes. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.RoamFlavoredMarkdown()`. +- Source: [`quartz-community/roam`](https://github.com/quartz-community/roam) +- Install: `npx quartz plugin add github:quartz-community/roam` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Search.md b/Local/storage/thlab-notes/worker/docs/plugins/Search.md new file mode 100644 index 0000000..c2947f2 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Search.md @@ -0,0 +1,44 @@ +--- +title: Search +description: Full-text search with tag filtering and keyboard navigation. +tags: + - plugin/component +image: https://images.unsplash.com/photo-1516382799247-87df95d790b7 +repository: "[quartz-community/search](https://github.com/quartz-community/search)" +enabled: true +required: false +--- + +Full-text search functionality. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +See [[full-text search]] for detailed usage information. + +## Configuration + +This plugin accepts the following configuration options: + +- `enablePreview`: Whether to show a preview of the page content in search results. Defaults to `true`. +- `fieldPriority`: An array specifying the priority order for search fields. Defaults to `["title", "content", "tags"]`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/search + enabled: true + options: + enablePreview: true + fieldPriority: + - title + - content + - tags +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.Search()`. +- Source: [`quartz-community/search`](https://github.com/quartz-community/search) +- Install: `npx quartz plugin add github:quartz-community/search` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Spacer.md b/Local/storage/thlab-notes/worker/docs/plugins/Spacer.md new file mode 100644 index 0000000..24f7cb4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Spacer.md @@ -0,0 +1,25 @@ +--- +title: Spacer +description: Flexible spacer for layout groups. +tags: + - plugin/component +image: +new-in-v5: true +repository: "[quartz-community/spacer](https://github.com/quartz-community/spacer)" +enabled: true +required: false +--- + +This plugin renders a flexible spacer element that pushes adjacent components apart within a layout group. It uses CSS `flex: 2 1 auto` to fill available space, making it useful for spacing out items in toolbars or sidebars (for example, separating the search bar from the darkmode toggle in the left sidebar toolbar). + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Component +- Function name: `ExternalPlugin.Spacer()`. +- Source: [`quartz-community/spacer`](https://github.com/quartz-community/spacer) +- Install: `npx quartz plugin add github:quartz-community/spacer` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/StackedPages.md b/Local/storage/thlab-notes/worker/docs/plugins/StackedPages.md new file mode 100644 index 0000000..9148ca3 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/StackedPages.md @@ -0,0 +1,61 @@ +--- +title: StackedPages +description: Andy Matuschak-style stacked sliding panes. +tags: + - plugin/component +image: +new-in-v5: true +repository: "[quartz-community/stacked-pages](https://github.com/quartz-community/stacked-pages)" +enabled: true +required: false +--- + +Andy Matuschak-style stacked pages (sliding panes). Clicking internal links opens pages side by side in a horizontal stack, allowing you to trace your path through your notes. Each pane shows a full page and can be individually scrolled or closed. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Usage + +Once enabled, clicking any internal link on a page opens the linked page as a new pane to the right instead of navigating away. The URL updates with a `#stacked=slug1,slug2` hash encoding your current stack, so you can share or bookmark a specific trail of pages. + +Stacked pages are disabled on mobile by default (below 800px) since horizontal panning doesn't work well on small screens. On mobile, links navigate normally. + +### Interactions + +- **Click a link**: Opens the target page in a new pane to the right. If the maximum number of panes is reached, the leftmost pane is removed. +- **Close a pane**: Click the × button in the pane header to remove it from the stack. +- **Collapsed spines**: When panes overflow the viewport, earlier panes collapse to a thin vertical spine showing the page title. Click a spine to bring that pane back into focus. +- **Browser back/forward**: The full stack state is stored in the URL hash and integrated with browser history, so back/forward navigation works as expected. + +## Configuration + +This plugin accepts the following configuration options: + +- `maxTabs`: Maximum number of stacked panes visible at once. Defaults to `8`. +- `mobileBreakpoint`: Viewport width (in pixels) below which stacked pages are disabled and links navigate normally. Defaults to `800`. +- `showSpines`: Whether to show collapsed spine headers when panes overflow the viewport. Defaults to `true`. +- `animateTransitions`: Whether to animate pane open/close transitions. Defaults to `true`. + +### Default options + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/stacked-pages + enabled: true + layout: + position: afterBody + priority: 50 + display: all + options: + maxTabs: 8 + mobileBreakpoint: 800 + showSpines: true + animateTransitions: true +``` + +## API + +- Category: Component +- Function name: `ExternalPlugin.StackedPages()`. +- Source: [`quartz-community/stacked-pages`](https://github.com/quartz-community/stacked-pages) +- Install: `npx quartz plugin add github:quartz-community/stacked-pages` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/Static.md b/Local/storage/thlab-notes/worker/docs/plugins/Static.md new file mode 100644 index 0000000..494e1d8 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/Static.md @@ -0,0 +1,22 @@ +--- +title: Static +tags: + - plugin/emitter +image: +--- + +This plugin emits all static resources needed by Quartz. This is used, for example, for fonts and images that need a stable position, such as banners and icons. The plugin respects the `ignorePatterns` in the global [[configuration]]. + +> [!important] +> This is different from [[Assets]]. The resources from the [[Static]] plugin are located under `quartz/static`, whereas [[Assets]] renders all static resources under `content` and is used for images, videos, audio, etc. that are directly referenced by your markdown content. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `Plugin.Static()` (internal plugin). +- Source: [`quartz/plugins/emitters/static.ts`](https://github.com/jackyzha0/quartz/blob/v5/quartz/plugins/emitters/static.ts). diff --git a/Local/storage/thlab-notes/worker/docs/plugins/SyntaxHighlighting.md b/Local/storage/thlab-notes/worker/docs/plugins/SyntaxHighlighting.md new file mode 100644 index 0000000..7eae436 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/SyntaxHighlighting.md @@ -0,0 +1,31 @@ +--- +title: SyntaxHighlighting +description: Syntax highlighting for code blocks. +tags: + - plugin/transformer +image: https://images.unsplash.com/photo-1580569214296-5cf2bffc5ccd +repository: "[quartz-community/syntax-highlighting](https://github.com/quartz-community/syntax-highlighting)" +enabled: true +required: false +--- + +This plugin is used to add syntax highlighting to code blocks in Quartz. See [[syntax highlighting]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `theme`: a separate id of one of the [themes bundled with Shikiji](https://shikiji.netlify.app/themes). One for light mode and one for dark mode. Defaults to `theme: { light: "github-light", dark: "github-dark" }`. +- `keepBackground`: If set to `true`, the background of the Shikiji theme will be used. With `false` (default) the Quartz theme color for background will be used instead. +- `clipboard`: Whether to add a copy-to-clipboard button to code blocks. Defaults to `true`. +- `tokenClassification`: Whether to add semantic token classification CSS classes to code tokens. Defaults to `true`. + +In addition, you can further override the colours in the `quartz/styles/syntax.scss` file. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.SyntaxHighlighting()`. +- Source: [`quartz-community/syntax-highlighting`](https://github.com/quartz-community/syntax-highlighting) +- Install: `npx quartz plugin add github:quartz-community/syntax-highlighting` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/TableOfContents.md b/Local/storage/thlab-notes/worker/docs/plugins/TableOfContents.md new file mode 100644 index 0000000..a856fb0 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/TableOfContents.md @@ -0,0 +1,34 @@ +--- +title: TableOfContents +description: Generates and renders a table of contents from headings. +tags: + - plugin/transformer + - plugin/component +image: https://images.unsplash.com/photo-1768527338896-3765921e992d +repository: "[quartz-community/table-of-contents](https://github.com/quartz-community/table-of-contents)" +enabled: true +required: false +--- + +This plugin generates a table of contents (TOC) for Markdown documents. See [[table of contents]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `maxDepth`: Limits the depth of headings included in the TOC, ranging from `1` (top level headings only) to `6` (all heading levels). Default is `3`. +- `minEntries`: The minimum number of heading entries required for the TOC to be displayed. Default is `1`. +- `showByDefault`: If `true` (default), the TOC should be displayed by default. Can be overridden by frontmatter settings. +- `collapseByDefault`: If `true`, the TOC will start in a collapsed state. Default is `false`. +- `layout`: The visual layout of the TOC component. Can be `"modern"` or `"legacy"`. Default is `"modern"`. + +> [!warning] +> This plugin needs the `Plugin.TableOfContents` component in `quartz.config.yaml` to determine where to display the TOC. Without it, nothing will be displayed. They should always be added or removed together. + +## API + +- Category: Transformer, Component +- Function name: `ExternalPlugin.TableOfContentsTransformer()`. +- Source: [`quartz-community/table-of-contents`](https://github.com/quartz-community/table-of-contents) +- Install: `npx quartz plugin add github:quartz-community/table-of-contents` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/TagList.md b/Local/storage/thlab-notes/worker/docs/plugins/TagList.md new file mode 100644 index 0000000..4fcde5c --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/TagList.md @@ -0,0 +1,24 @@ +--- +title: TagList +description: Renders tags as clickable links. +tags: + - plugin/component +image: +repository: "[quartz-community/tag-list](https://github.com/quartz-community/tag-list)" +enabled: false +required: false +--- + +This plugin renders the page's tags as a list of clickable links. Each tag links to its corresponding [[TagPage|tag page]], making it easy for readers to browse related content by topic. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Component +- Function name: `ExternalPlugin.TagList()`. +- Source: [`quartz-community/tag-list`](https://github.com/quartz-community/tag-list) +- Install: `npx quartz plugin add github:quartz-community/tag-list` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/TagPage.md b/Local/storage/thlab-notes/worker/docs/plugins/TagPage.md new file mode 100644 index 0000000..2a6b808 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/TagPage.md @@ -0,0 +1,28 @@ +--- +title: TagPage +description: Generates listing pages for tags. +tags: + - plugin/pageType +image: +repository: "[quartz-community/tag-page](https://github.com/quartz-community/tag-page)" +enabled: true +required: false +--- + +This plugin is a page type plugin that emits dedicated pages for each tag used in the content. It uses the `default` [[layout#Page Frames|page frame]] (three-column layout with sidebars). See [[folder and tag listings]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `numPages`: The maximum number of pages to display per tag before showing a "see more" link. Defaults to `10`. +- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order. Requires a TS override. +- `prefixTags`: If `true`, generated tag page titles are prefixed with "Tag: " (e.g. "Tag: recipes"). Defaults to `false`. + +## API + +- Category: Page Type +- Function name: `ExternalPlugin.TagPage()`. +- Source: [`quartz-community/tag-page`](https://github.com/quartz-community/tag-page) +- Install: `npx quartz plugin add github:quartz-community/tag-page` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/UnlistedPages.md b/Local/storage/thlab-notes/worker/docs/plugins/UnlistedPages.md new file mode 100644 index 0000000..7cb6079 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/UnlistedPages.md @@ -0,0 +1,68 @@ +--- +title: UnlistedPages +description: Hides pages from navigation and indexes while still publishing them. +tags: + - plugin/transformer +image: +new-in-v5: true +repository: "[quartz-community/unlisted-pages](https://github.com/quartz-community/unlisted-pages)" +enabled: true +required: false +--- + +Zero-config transformer that makes `unlisted: true` in a page's frontmatter a first-class way to opt out of every listing surface on your site. The page is still emitted as HTML and remains accessible by direct URL, but is absent from `contentIndex.json`, RSS, sitemap, graph, explorer, search, backlinks, recent notes, folder listings, and tag listings. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Usage + +Add an `unlisted` field to any page's frontmatter: + +```yaml +--- +title: My Draft +unlisted: true +--- +``` + +That's it. Every Quartz v5 plugin that respects the `file.data.unlisted` convention will then hide the page. + +## What each plugin does + +| Plugin | Behavior when `unlisted: true` | +| -------------- | -------------------------------------------------------------------------- | +| `ContentIndex` | Page absent from `contentIndex.json`, `sitemap.xml`, and the RSS feed. | +| `Search` | Page absent from search results (derived from `contentIndex.json`). | +| `Graph` | Page absent from graph nodes and edges (derived from `contentIndex.json`). | +| `Explorer` | Page absent from the sidebar file tree (derived from `contentIndex.json`). | +| `Backlinks` | Page never appears as a backlink source on other pages. | +| `RecentNotes` | Page absent from the recent notes list. | +| `FolderPage` | Page absent from folder listings and folder discovery. | +| `TagPage` | Page absent from tag discovery and tag listings. | + +In every case, the page's HTML is still emitted and accessible by direct URL. + +## Configuration + +Zero options. Just enable it. + +```yaml title="quartz.config.yaml" +- source: github:quartz-community/unlisted-pages + enabled: true +``` + +## Interaction with [[EncryptedPages]] + +The [[EncryptedPages]] plugin also sets `file.data.unlisted` when its `unlistWhenEncrypted: true` option is set or when a page has `unlisted: true` in frontmatter. The two plugins compose cleanly: + +- If you install only `UnlistedPages`: any page with `unlisted: true` in frontmatter is hidden from listing surfaces. Encryption is independent. +- If you install only `EncryptedPages`: `unlisted: true` only takes effect on pages that are also encrypted (have a password). Non-encrypted pages with `unlisted: true` are silently ignored. +- If you install both: `unlisted: true` works for every page, encrypted or not. This is the recommended setup for sites that use encrypted pages. + +## API + +- Category: Transformer +- Function name: `ExternalPlugin.UnlistedPages()`. +- Source: [`quartz-community/unlisted-pages`](https://github.com/quartz-community/unlisted-pages) +- Install: `npx quartz plugin add github:quartz-community/unlisted-pages` diff --git a/Local/storage/thlab-notes/worker/docs/plugins/index.md b/Local/storage/thlab-notes/worker/docs/plugins/index.md new file mode 100644 index 0000000..ff75ad1 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/plugins/index.md @@ -0,0 +1,146 @@ +--- +title: Plugins +image: +--- + +Quartz's functionality is provided by a collection of first-party community plugins. Each plugin can be enabled, disabled, and configured via `quartz.config.yaml`. See [[configuration#Plugins|Configuration]] for details on how to manage plugins. + +> [!info] Internal vs Community Plugins +> Quartz has two kinds of plugins: +> +> - **Community plugins** are standalone repositories under [`quartz-community`](https://github.com/quartz-community). In TS overrides, they use `ExternalPlugin.X()` (imported from `.quartz/plugins`). +> - **Internal plugins** are built into Quartz core (Assets, Static, ComponentResources, NotFoundPage). In TS overrides, they use `Plugin.X()` (imported from `./quartz/plugins`). + +## Plugin types + +Quartz plugins fall into several categories: + +- **Transformers** process content during the build, e.g. parsing frontmatter, highlighting syntax, or resolving links. +- **Filters** decide which content files to include or exclude from the output. +- **Page Types** generate HTML pages — one per content file, folder, tag, canvas, or bases view. +- **Components** render UI elements in the page layout (sidebars, headers, footers, etc.). + +## First-party plugins + +```base +filters: + and: + - file.ext == "md" + - file.inFolder("plugins") + - "!file.name.startsWith('index')" + - "!file.name.contains('Demo')" + - "!file.name.contains('Static')" + - "!file.name.contains('Assets')" + - "!file.name.contains('ComponentResources')" + - "!file.name.contains('NotFoundPage')" +formulas: + category: | + if(file.hasTag("plugin/transformer"), "Transformer", + if(file.hasTag("plugin/filter"), "Filter", + if(file.hasTag("plugin/pageType"), "Page Type", + if(file.hasTag("plugin/emitter"), "Emitter", + if(file.hasTag("plugin/component"), "Component", + "Other"))))) +properties: + title: + displayName: Plugin + repository: + displayName: Repository + enabled: + displayName: Enabled + required: + displayName: Required + description: + displayName: Description +views: + - type: table + name: All Plugins + groupBy: + property: formula.category + direction: ASC + order: + - title + - repository + - enabled + - required + - description + sort: + - property: formula.category + direction: ASC + - property: title + direction: ASC + - type: table + name: Transformers + filters: + and: + - file.hasTag("plugin/transformer") + order: + - title + - repository + - enabled + - required + - description + sort: + - property: title + direction: ASC + - type: table + name: Filters + filters: + and: + - file.hasTag("plugin/filter") + order: + - title + - repository + - enabled + - required + - description + sort: + - property: title + direction: ASC + - type: table + name: Page Types + filters: + and: + - file.hasTag("plugin/pageType") + order: + - title + - repository + - enabled + - required + - description + sort: + - property: title + direction: ASC + - type: table + name: Emitters + filters: + and: + - file.hasTag("plugin/emitter") + order: + - title + - repository + - enabled + - required + - description + sort: + - property: title + direction: ASC + - type: table + name: Components + filters: + and: + - file.hasTag("plugin/component") + order: + - title + - repository + - enabled + - required + - description + sort: + - property: title + direction: ASC + +``` + +> [!note] Multi-category plugins +> Some plugins span multiple categories. **TableOfContents** is both a transformer and a component. **EncryptedPages** is a transformer, emitter, and component. They appear in each relevant category above. diff --git a/Local/storage/thlab-notes/worker/docs/showcase.md b/Local/storage/thlab-notes/worker/docs/showcase.md new file mode 100644 index 0000000..20fc325 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/showcase.md @@ -0,0 +1,23 @@ +--- +title: "Quartz Showcase" +--- + +Want to see what Quartz can do? Here are some cool community gardens: + +- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/) +- [Jacky Zhao's Garden](https://jzhao.xyz/) +- [Aaron Pham's Garden](https://aarnphm.xyz/) +- [The Pond](https://turntrout.com/welcome) +- [Eilleen's Everything Notebook](https://quartz.eilleeenz.com/) +- [Morrowind Modding Wiki](https://morrowind-modding.github.io/) +- [Stanford CME 302 Numerical Linear Algebra](https://ericdarve.github.io/NLA/) +- [Socratica Toolbox](https://toolbox.socratica.info/) +- [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/) +- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/) +- [Brandon Boswell's Garden](https://brandonkboswell.com) +- [Data Engineering Vault: A Second Brain Knowledge Network](https://vault.ssp.sh/) +- [🪴Aster's notebook](https://notes.asterhu.com) +- [Gatekeeper Wiki](https://www.gatekeeper.wiki) +- [Ellie's Notes](https://ellie.wtf) +- [Eledah's Crystalline](https://blog.eledah.ir/) +- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com) diff --git a/Local/storage/thlab-notes/worker/docs/tags/component.md b/Local/storage/thlab-notes/worker/docs/tags/component.md new file mode 100644 index 0000000..57592e8 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/tags/component.md @@ -0,0 +1,5 @@ +--- +title: Components +--- + +Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information. diff --git a/Local/storage/thlab-notes/worker/docs/tags/plugin.md b/Local/storage/thlab-notes/worker/docs/tags/plugin.md new file mode 100644 index 0000000..298ff16 --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/tags/plugin.md @@ -0,0 +1,3 @@ +--- +title: Plugins +--- diff --git a/Local/storage/thlab-notes/worker/docs/troubleshooting.md b/Local/storage/thlab-notes/worker/docs/troubleshooting.md new file mode 100644 index 0000000..ba06cfc --- /dev/null +++ b/Local/storage/thlab-notes/worker/docs/troubleshooting.md @@ -0,0 +1,172 @@ +--- +title: Troubleshooting +--- + +Common issues and solutions when working with Quartz. + +## Build Errors + +### `Could not resolve ...` or missing module errors + +This usually means a plugin is not installed. Run: + +```bash +npx quartz plugin install +``` + +This restores all plugins from your `quartz.lock.json` to `.quartz/plugins/`. + +### `tsc` type errors after updating + +After running `npx quartz upgrade`, type errors can occur if the update changed internal APIs that your `quartz.ts` overrides depend on. Check the changelog for breaking changes and update your overrides accordingly. + +### Build is slow + +Try increasing concurrency: + +```bash +npx quartz build --concurrency 8 +# or the shorthand: +npx quartz build -c 8 +``` + +The default uses all available CPU cores. If you're on a memory-constrained environment (CI), reducing concurrency may actually help. + +## Plugin Issues + +### Plugin not loading after installation + +1. Verify the plugin appears in `quartz.config.yaml` under `plugins:` +2. Check that `enabled: true` is set +3. Run `npx quartz plugin list` to confirm it's installed +4. Run `npx quartz plugin install --latest --dry-run` to verify plugin health + +### Plugin options not taking effect + +Make sure your YAML indentation is correct. Options must be nested under the plugin entry: + +```yaml title="quartz.config.yaml" +plugins: + - source: github:quartz-community/some-plugin + enabled: true + options: + myOption: value # correct: nested under options +``` + +A common mistake is putting options at the wrong indentation level. + +### `ExternalPlugin.X is not a function` + +This means the plugin is referenced in `quartz.ts` but not installed. Either: + +- Install it: `npx quartz plugin add github:quartz-community/plugin-name` +- Or remove the reference from `quartz.ts` + +### Plugins fail to build on a fresh clone + +> [!important] +> Most community plugins now ship with a pre-built `dist/` directory and skip the build step entirely. The build failure scenario described below mainly applies to plugins in development or older plugins that haven't adopted pre-built distribution. + +On a brand-new clone, `npx quartz plugin install` (or the plugin step run automatically by `npx quartz create`) may report a handful of plugins failing to build — typically around 10–15 of them. The git clone and checkout still succeed, but `npm run build` inside the plugin errors out. + +This happens because `quartz.lock.json` pins each plugin to a specific commit, and those older plugin commits may have been authored against earlier versions of `@quartz-community/types` / `@quartz-community/utils` whose published artifacts are no longer shipped in the dependency's git repo. The plugin's `tsup`/`tsc` build then cannot resolve the expected type declarations. + +Fix it by refreshing all plugins to the latest commit on their default branch: + +```bash +npx quartz plugin install --latest +``` + +This rewrites `quartz.lock.json` with the newest commits (which in turn pin newer `@quartz-community/*` versions whose built output is available), and rebuilds every plugin from scratch. After this step, subsequent `npx quartz plugin install` calls will restore cleanly from the refreshed lockfile. + +### `plugin install` hangs, OOMs, or fails on low-end hardware + +> [!note] +> Pre-built plugins are much faster and lighter on resources because they skip the `npm install` and `npm run build` steps. + +By default, `npx quartz plugin install` clones, fetches, and builds plugins in parallel across all your CPU cores. Each parallel worker may run its own `npm install` and `npm run build`, which is memory-intensive. On low-end laptops, Raspberry Pi, small VPS instances, or restrictive CI runners this can exhaust RAM, trigger the OOM killer, or make the system appear to hang. + +Lower the parallelism with `--concurrency` / `-c`: + +```bash +# Install one plugin at a time (safest, slowest) +npx quartz plugin install --latest -c 1 + +# Two at a time — usually works on 4 GB machines +npx quartz plugin install --latest --concurrency 2 +``` + +The same flag works for `plugin add` and the deprecated aliases (`plugin update`, `plugin restore`, `plugin check`, `plugin resolve`): + +```bash +npx quartz plugin add github:quartz-community/some-plugin -c 1 +``` + +If `plugin install` consistently fails near the same plugin with `-c 1`, the issue is likely with that specific plugin's build, not with concurrency — try running `--verbose` to get detailed error output, and check the plugin's own repository for known issues. + +## Content Issues + +### Notes not showing up + +- Check that the file is in the `content/` folder +- Check that `draft: true` is not set in the frontmatter (the [[RemoveDrafts]] plugin filters these out) +- If using [[ExplicitPublish]], make sure `publish: true` is set in frontmatter +- Check your [[configuration]] `ignorePatterns` to make sure the file path is not excluded + +### Wikilinks not resolving + +- Make sure the [[ObsidianFlavoredMarkdown]] plugin is enabled +- Verify the target note exists in your content folder +- Check for case sensitivity issues in filenames + +### Images not displaying + +- Ensure images are in a folder that Quartz processes (typically `content/` or a subfolder) +- Check that the image path in your Markdown matches the actual file location +- The [[Assets]] emitter must be enabled (it is by default) + +## GitHub Sync Issues + +### `fatal: --[no-]autostash option is only valid with --rebase` + +You may have an outdated version of `git`. Update git to resolve this. + +### `fatal: The remote end hung up unexpectedly` + +This is usually due to Git's default buffer size being too small for your content. Increase it: + +```bash +git config http.postBuffer 524288000 +``` + +### Merge conflicts during sync + +If `npx quartz sync` encounters merge conflicts: + +1. Resolve the conflicts in your editor +2. Run `git add .` and `git commit` to complete the merge +3. Run `npx quartz sync --no-pull` to push + +If you want to start over, run `npx quartz restore` to recover your content from the cache. + +## Development Server Issues + +### Hot reload not working + +- Make sure you're using `--serve` mode: `npx quartz build --serve` +- Check that port 3001 (WebSocket) is not blocked — this is the default `--wsPort` used for hot reload notifications +- If developing remotely, use `--remoteDevHost` to set the correct WebSocket URL + +### Port already in use + +Change the port: + +```bash +npx quartz build --serve --port 3000 +``` + +## Still stuck? + +- Check the [GitHub Issues](https://github.com/jackyzha0/quartz/issues) for similar problems +- Ask in the [Discord Community](https://discord.gg/cRFFHYye7t) +- Run your command with `--verbose` for more detailed error output diff --git a/Local/storage/thlab-notes/worker/entrypoint.sh b/Local/storage/thlab-notes/worker/entrypoint.sh new file mode 100644 index 0000000..ed9ccb5 --- /dev/null +++ b/Local/storage/thlab-notes/worker/entrypoint.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -e + +# Configuration from environment variables +VAULT_REPO="${VAULT_REPO:-}" +VAULT_BRANCH="${VAULT_BRANCH:-main}" +SYNC_INTERVAL="${SYNC_INTERVAL:-30}" +CONTENT_DIR="/usr/src/app/content" + +# Logging function +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +# Initialize vault content +init_vault() { + log "Initializing vault from $VAULT_REPO (branch: $VAULT_BRANCH)" + + if [ -z "$VAULT_REPO" ]; then + log "ERROR: VAULT_REPO not set. Using pre-existing content." + return 0 + fi + + # Remove existing content if it exists and is a git repo + if [ -d "$CONTENT_DIR/.git" ]; then + log "Content directory is a git repo, pulling latest changes..." + cd "$CONTENT_DIR" + git fetch origin "$VAULT_BRANCH" + git reset --hard "origin/$VAULT_BRANCH" + cd /usr/src/app + else + log "Content directory is not a git repo, cloning..." + rm -rf "$CONTENT_DIR" + git clone --depth 1 --branch "$VAULT_BRANCH" "$VAULT_REPO" "$CONTENT_DIR" + fi + + log "Vault synchronized successfully" +} + +# Sync vault periodically in background +sync_vault_loop() { + while true; do + sleep "$SYNC_INTERVAL" + log "Checking for vault updates..." + + if [ -d "$CONTENT_DIR/.git" ]; then + cd "$CONTENT_DIR" + # Check if there are remote changes + git fetch origin "$VAULT_BRANCH" 2>/dev/null || continue + + LOCAL=$(git rev-parse HEAD) + REMOTE=$(git rev-parse "origin/$VAULT_BRANCH") + + if [ "$LOCAL" != "$REMOTE" ]; then + log "Vault changes detected, pulling..." + git reset --hard "origin/$VAULT_BRANCH" + log "Vault updated. Note: Quartz build may need restart for changes to appear." + else + log "Vault is up to date" + fi + cd /usr/src/app + fi + done +} + +# Initialize once on startup +init_vault + +# Start sync loop in background +sync_vault_loop & +SYNC_PID=$! + +log "Starting Quartz..." +# Run Quartz with proper signal handling +trap "kill $SYNC_PID 2>/dev/null || true" EXIT TERM INT +exec npx quartz build --serve + diff --git a/Local/storage/thlab-notes/worker/globals.d.ts b/Local/storage/thlab-notes/worker/globals.d.ts new file mode 100644 index 0000000..6cf30f8 --- /dev/null +++ b/Local/storage/thlab-notes/worker/globals.d.ts @@ -0,0 +1,17 @@ +export declare global { + interface Document { + addEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void, + ): void + removeEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void, + ): void + dispatchEvent(ev: CustomEventMap[K] | UIEvent): void + } + interface Window { + spaNavigate(url: URL, isBack: boolean = false) + addCleanup(fn: (...args: any[]) => void) + } +} diff --git a/Local/storage/thlab-notes/worker/index.d.ts b/Local/storage/thlab-notes/worker/index.d.ts new file mode 100644 index 0000000..1dc0e2f --- /dev/null +++ b/Local/storage/thlab-notes/worker/index.d.ts @@ -0,0 +1,16 @@ +declare module "*.scss" { + const content: string + export = content +} + +// dom custom event +interface CustomEventMap { + prenav: CustomEvent<{}> + nav: CustomEvent<{ url: FullSlug }> + themechange: CustomEvent<{ theme: "light" | "dark" }> + readermodechange: CustomEvent<{ mode: "on" | "off" }> + render: CustomEvent<{}> +} + +type ContentIndex = Record +declare const fetchData: Promise diff --git a/Local/storage/thlab-notes/worker/package-lock.json b/Local/storage/thlab-notes/worker/package-lock.json new file mode 100644 index 0000000..c67ea31 --- /dev/null +++ b/Local/storage/thlab-notes/worker/package-lock.json @@ -0,0 +1,5845 @@ +{ + "name": "@jackyzha0/quartz", + "version": "5.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@jackyzha0/quartz", + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.11.0", + "@floating-ui/dom": "^1.7.4", + "@myriaddreamin/rehype-typst": "^0.6.0", + "@napi-rs/simple-git": "0.1.22", + "ansi-truncate": "^1.4.0", + "async-mutex": "^0.5.0", + "chokidar": "^5.0.0", + "esbuild-sass-plugin": "^3.6.0", + "github-slugger": "^2.0.0", + "globby": "^16.1.0", + "hast-util-to-jsx-runtime": "^2.3.6", + "isomorphic-git": "^1.36.3", + "lightningcss": "^1.31.1", + "micromorph": "^0.4.5", + "minimatch": "^10.1.1", + "preact": "^10.28.2", + "preact-render-to-string": "^6.6.5", + "pretty-bytes": "^7.1.0", + "pretty-time": "^1.1.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "rfdc": "^1.4.1", + "serve-handler": "^6.1.6", + "sharp": "^0.34.5", + "source-map-support": "^0.5.21", + "to-vfile": "^8.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3", + "workerpool": "^10.0.1", + "ws": "^8.19.0", + "yaml": "^2.8.2", + "yargs": "^18.0.0" + }, + "bin": { + "quartz": "quartz/bootstrap-cli.mjs" + }, + "devDependencies": { + "@quartz-community/types": "github:quartz-community/types", + "@quartz-community/utils": "github:quartz-community/utils", + "@types/hast": "^3.0.4", + "@types/node": "^25.0.10", + "@types/pretty-time": "^1.1.5", + "@types/source-map-support": "^0.5.10", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.35", + "esbuild": "^0.27.2", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true + }, + "node_modules/@clack/core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", + "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", + "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.5.0", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@myriaddreamin/rehype-typst": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/rehype-typst/-/rehype-typst-0.6.0.tgz", + "integrity": "sha512-WQpr2j7OYtyc2Q1WOqi1wzYrBaeuAWT1Cn1Ki6VPsKoWH7O86/+zKOqltdgMpYdkav1uXYs3RfO5Ir8h0WkZyQ==", + "license": "MIT", + "dependencies": { + "@myriaddreamin/typst-ts-node-compiler": "^0.6.0", + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "https-proxy-agent": "^7.0.2", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler/-/typst-ts-node-compiler-0.6.0.tgz", + "integrity": "sha512-C40MzRKZ8pDWzrS7VOtTypGyFaHTuZFFx3o/uQ6ryS2GqZkK3vGox4lIpR7ct11UHiAjQNR3LFQ5WjQ7P3niBQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@myriaddreamin/typst-ts-node-compiler-android-arm-eabi": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-android-arm64": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-darwin-arm64": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-darwin-x64": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-linux-x64-musl": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc": "0.6.0", + "@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc": "0.6.0" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-android-arm-eabi": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-android-arm-eabi/-/typst-ts-node-compiler-android-arm-eabi-0.6.0.tgz", + "integrity": "sha512-Gfrf9Fky5iYtutGWYwqRC4gvllK1p1q6YELCbycI47NCFptONI++3dfub4PixWRn9m8NrmaNFIBQSyLHWsvbLw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-android-arm64": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-android-arm64/-/typst-ts-node-compiler-android-arm64-0.6.0.tgz", + "integrity": "sha512-EzO6W4xELC6at30hSkkOp5BveszwCmTWceu0PMh6lPxeQF1vnjxUK60MLFfJ40zb1TOXsj4l2pbdBoGqLznC1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-darwin-arm64": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-darwin-arm64/-/typst-ts-node-compiler-darwin-arm64-0.6.0.tgz", + "integrity": "sha512-8tR1GqFr+q4rNZm8z0230eF7eRCVCSaUefDw1+Qw8EnDPIvwEP8bT0/u2YqHmxthfVfs1msV8hDpRKVeBa6E3g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-darwin-x64": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-darwin-x64/-/typst-ts-node-compiler-darwin-x64-0.6.0.tgz", + "integrity": "sha512-eytv5ifNvhux9naqEb+4pu1Z4ghQBWiybP4lT/aB44I9H5xjmtYQxiKwNBz54am6RLiMcyLpw/xFdeB13bsdWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf/-/typst-ts-node-compiler-linux-arm-gnueabihf-0.6.0.tgz", + "integrity": "sha512-b20do+PmbsYq07QlTW8uLU3MaoAm6DSCx1IrCEAlUpNH+/29x51Rvyq5JeRrYVOtkR6BxPzyhCM79r5jOkewbQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu/-/typst-ts-node-compiler-linux-arm64-gnu-0.6.0.tgz", + "integrity": "sha512-AM92MVfEbISYvIA8NwPl2l78nOZIh5er5qQ/NZw2kx4YgTKgklJINEPHXm/aAk7PcpX7G10P45D/xGd5KpX9HQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl/-/typst-ts-node-compiler-linux-arm64-musl-0.6.0.tgz", + "integrity": "sha512-nSokVjKQR0ZH7Jub53q7he89+m72RSbL97exSedkB4OdZAi9tAxGFIgceGJuN5AC+DiNtMmqsPwlJiERUjgPhQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu/-/typst-ts-node-compiler-linux-x64-gnu-0.6.0.tgz", + "integrity": "sha512-3Y2ORiYuCTzQkiHSCHWiGuzTBbNvHTB2lCr3DDsZdvTZ2LZMifPwwICN26X3tlnt6GyC3o/ejZBcMnfNqYbdCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl/-/typst-ts-node-compiler-linux-x64-musl-0.6.0.tgz", + "integrity": "sha512-b+kTb4vI0sFTkPtIAUE+UqjhZ4kTiAkh4F/2QKnFitAsURlLcRwTcMc9NJm6SXwW1OM0nPj1IGTfUOFpqLOIPQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc/-/typst-ts-node-compiler-win32-arm64-msvc-0.6.0.tgz", + "integrity": "sha512-04omIPrXSsRKu4XDhj1WZ9uMjdcFcejBGzyOEV351HVDqg5kxgDB32iG3oLySLrzEcbi9WwI5Si46WrW0wh4mA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc/-/typst-ts-node-compiler-win32-x64-msvc-0.6.0.tgz", + "integrity": "sha512-w5UEmXSZ+Eg7Y04EzjgqeHUo7P8bNz9S1c4CUfLrbfZvbTmYNjA0WeqZJ3+tV03BSVxiPiVhrfo95sLqKISNrg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.22.tgz", + "integrity": "sha512-bMVoAKhpjTOPHkW/lprDPwv5aD4R4C3Irt8vn+SKA9wudLe9COLxOhurrKRsxmZccUbWXRF7vukNeGUAj5P8kA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/simple-git-android-arm-eabi": "0.1.22", + "@napi-rs/simple-git-android-arm64": "0.1.22", + "@napi-rs/simple-git-darwin-arm64": "0.1.22", + "@napi-rs/simple-git-darwin-x64": "0.1.22", + "@napi-rs/simple-git-freebsd-x64": "0.1.22", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.22", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.22", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.22", + "@napi-rs/simple-git-linux-ppc64-gnu": "0.1.22", + "@napi-rs/simple-git-linux-s390x-gnu": "0.1.22", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.22", + "@napi-rs/simple-git-linux-x64-musl": "0.1.22", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.22", + "@napi-rs/simple-git-win32-ia32-msvc": "0.1.22", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.22" + } + }, + "node_modules/@napi-rs/simple-git-android-arm-eabi": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.22.tgz", + "integrity": "sha512-JQZdnDNm8o43A5GOzwN/0Tz3CDBQtBUNqzVwEopm32uayjdjxev1Csp1JeaqF3v9djLDIvsSE39ecsN2LhCKKQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-android-arm64": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.22.tgz", + "integrity": "sha512-46OZ0SkhnvM+fapWjzg/eqbJvClxynUpWYyYBn4jAj7GQs1/Yyc8431spzDmkA8mL0M7Xo8SmbkzTDE7WwYAfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-arm64": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.22.tgz", + "integrity": "sha512-zH3h0C8Mkn9//MajPI6kHnttywjsBmZ37fhLX/Fiw5XKu84eHA6dRyVtMzoZxj6s+bjNTgaMgMUucxPn9ktxTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-x64": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.22.tgz", + "integrity": "sha512-GZN7lRAkGKB6PJxWsoyeYJhh85oOOjVNyl+/uipNX8bR+mFDCqRsCE3rRCFGV9WrZUHXkcuRL2laIRn7lLi3ag==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-freebsd-x64": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-freebsd-x64/-/simple-git-freebsd-x64-0.1.22.tgz", + "integrity": "sha512-xyqX1C5I0WBrUgZONxHjZH5a4LqQ9oki3SKFAVpercVYAcx3pq6BkZy1YUOP4qx78WxU1CCNfHBN7V+XO7D99A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.22.tgz", + "integrity": "sha512-4LOtbp9ll93B9fxRvXiUJd1/RM3uafMJE7dGBZGKWBMGM76+BAcCEUv2BY85EfsU/IgopXI6n09TycRfPWOjxA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.22.tgz", + "integrity": "sha512-GVOjP/JjCzbQ0kSqao7ctC/1sodVtv5VF57rW9BFpo2y6tEYPCqHnkQkTpieuwMNe+TVOhBUC1+wH0d9/knIHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-musl": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.22.tgz", + "integrity": "sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-ppc64-gnu": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-ppc64-gnu/-/simple-git-linux-ppc64-gnu-0.1.22.tgz", + "integrity": "sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-s390x-gnu": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-s390x-gnu/-/simple-git-linux-s390x-gnu-0.1.22.tgz", + "integrity": "sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-gnu": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.22.tgz", + "integrity": "sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-musl": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.22.tgz", + "integrity": "sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.22.tgz", + "integrity": "sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-ia32-msvc": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-ia32-msvc/-/simple-git-win32-ia32-msvc-0.1.22.tgz", + "integrity": "sha512-Gqr9Y0gs6hcNBA1IXBpoqTFnnIoHuZGhrYqaZzEvGMLrTrpbXrXVEtX3DAAD2RLc1b87CPcJ49a7sre3PU3Rfw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-x64-msvc": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.22.tgz", + "integrity": "sha512-hQjcreHmUcpw4UrtkOron1/TQObfe484lxiXFLLUj7aWnnnOVs1mnXq5/Bo9+3NYZldFpFRJPdPBeHCisXkKJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@quartz-community/types": { + "version": "0.2.1", + "resolved": "git+ssh://git@github.com/quartz-community/types.git#d413569b02ff0cd9a7e8d74dee125d20a5e5ec72", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + }, + "peerDependencies": { + "unified": "^11.0.5", + "vfile": "^6.0.3" + }, + "peerDependenciesMeta": { + "unified": { + "optional": true + }, + "vfile": { + "optional": true + } + } + }, + "node_modules/@quartz-community/utils": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/quartz-community/utils.git#e09d8b0bbccb46073b23c00affaf3e40a2c3b0c4", + "dev": true, + "license": "MIT", + "dependencies": { + "@quartz-community/types": "github:quartz-community/types" + }, + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + }, + "peerDependencies": { + "github-slugger": "^2.0.0", + "hast-util-to-jsx-runtime": "^2.3.6", + "preact": "^10.0.0" + }, + "peerDependenciesMeta": { + "github-slugger": { + "optional": true + }, + "hast-util-to-jsx-runtime": { + "optional": true + } + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pretty-time": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pretty-time/-/pretty-time-1.1.5.tgz", + "integrity": "sha512-5yl+BYwmnRWZb783W8YYoHXvPY8q/rp7ctHBVaGBB9RxlzGpHNJ72tGQMK7TrUSnxzl1dbDcBDuBCSbtfnSQGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.6.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-truncate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-truncate/-/ansi-truncate-1.4.0.tgz", + "integrity": "sha512-p6d2MrNs/mbpdXFT08fGabIg4pbgnUbbhrsoFfxWV5L3zFKw7tUkYUxGY3xCGJUPohENM80Q4sWkl/VDEN3pZg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.1" + } + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "license": "Apache-2.0" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "license": "MIT", + "peer": true + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/esbuild-sass-plugin": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.6.0.tgz", + "integrity": "sha512-lzPJQSEXcnj5amBPPib5lBjsDNPzvdMnX+1Rf7eha9BIpLSM5Ad2pi+Rqg5CAlWMduCgLntS2hLAqG7v1fxWGw==", + "license": "MIT", + "dependencies": { + "resolve": "^1.22.11", + "sass": "^1.97.2" + }, + "peerDependencies": { + "esbuild": ">=0.27.2", + "sass-embedded": "^1.97.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.1.1.tgz", + "integrity": "sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isomorphic-git": { + "version": "1.37.2", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.2.tgz", + "integrity": "sha512-HCQBBKmXIMPdHgYGstSBNp6MNmVcMQBbUqJF8xfywFmlpNseO4KKex59YlXqNxhRxmv3fUZwvNWvMyOdc1VvhA==", + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/isomorphic-git/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromorph": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/micromorph/-/micromorph-0.4.5.tgz", + "integrity": "sha512-Erasr0xiDvDeEhh7B/k7RFTwwfaAX10D7BMorNpokkwDh6XsRLYWDPaWF1m5JQeMSkGdqlEtQ8s68NcdDWuGgw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/preact": { + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.6.tgz", + "integrity": "sha512-EfqZJytnjJldV+YaaqhthU2oXsEf5e+6rDv957p+zxAvNfFLQOPfvBOTncscQ+akzu6Wrl7s3Pa0LjUQmWJsGQ==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10 || >= 11.0.0-0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.3.tgz", + "integrity": "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.97.3", + "sass-embedded-android-arm": "1.97.3", + "sass-embedded-android-arm64": "1.97.3", + "sass-embedded-android-riscv64": "1.97.3", + "sass-embedded-android-x64": "1.97.3", + "sass-embedded-darwin-arm64": "1.97.3", + "sass-embedded-darwin-x64": "1.97.3", + "sass-embedded-linux-arm": "1.97.3", + "sass-embedded-linux-arm64": "1.97.3", + "sass-embedded-linux-musl-arm": "1.97.3", + "sass-embedded-linux-musl-arm64": "1.97.3", + "sass-embedded-linux-musl-riscv64": "1.97.3", + "sass-embedded-linux-musl-x64": "1.97.3", + "sass-embedded-linux-riscv64": "1.97.3", + "sass-embedded-linux-x64": "1.97.3", + "sass-embedded-unknown-all": "1.97.3", + "sass-embedded-win32-arm64": "1.97.3", + "sass-embedded-win32-x64": "1.97.3" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.3.tgz", + "integrity": "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.3.tgz", + "integrity": "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.3.tgz", + "integrity": "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.3.tgz", + "integrity": "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.3.tgz", + "integrity": "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.3.tgz", + "integrity": "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.3.tgz", + "integrity": "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.3.tgz", + "integrity": "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.3.tgz", + "integrity": "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.3.tgz", + "integrity": "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.3.tgz", + "integrity": "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.3.tgz", + "integrity": "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.3.tgz", + "integrity": "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.3.tgz", + "integrity": "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.3.tgz", + "integrity": "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.3.tgz", + "integrity": "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "peer": true, + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.3.tgz", + "integrity": "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.3.tgz", + "integrity": "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-vfile": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-8.0.0.tgz", + "integrity": "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg==", + "license": "MIT", + "dependencies": { + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT", + "peer": true + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.1.tgz", + "integrity": "sha512-NAnKwZJxWlj/U1cp6ZkEtPE+GQY1S6KtOS3AlCiPfPFLxV3m64giSp7g2LsNJxzYCocDT7TSl+7T0sgrDp3KoQ==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/Local/storage/thlab-notes/worker/package.json b/Local/storage/thlab-notes/worker/package.json new file mode 100644 index 0000000..950ea18 --- /dev/null +++ b/Local/storage/thlab-notes/worker/package.json @@ -0,0 +1,88 @@ +{ + "name": "@jackyzha0/quartz", + "description": "🌱 publish your digital garden and notes as a website", + "private": true, + "version": "5.0.0", + "type": "module", + "author": "jackyzha0 ", + "license": "MIT", + "homepage": "https://quartz.jzhao.xyz", + "repository": { + "type": "git", + "url": "https://github.com/jackyzha0/quartz.git" + }, + "scripts": { + "quartz": "./quartz/bootstrap-cli.mjs", + "docs": "npx quartz build --serve -d docs", + "check": "tsc --noEmit && npx prettier . --check", + "format": "npx prettier . --write", + "test": "tsx --test", + "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", + "install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts", + "prebuild": "npm run install-plugins" + }, + "engines": { + "npm": ">=10.9.2", + "node": ">=22" + }, + "keywords": [ + "site generator", + "ssg", + "digital-garden", + "markdown", + "blog", + "quartz" + ], + "bin": { + "quartz": "./quartz/bootstrap-cli.mjs" + }, + "dependencies": { + "@clack/prompts": "^0.11.0", + "@floating-ui/dom": "^1.7.4", + "@myriaddreamin/rehype-typst": "^0.6.0", + "@napi-rs/simple-git": "0.1.22", + "ansi-truncate": "^1.4.0", + "async-mutex": "^0.5.0", + "chokidar": "^5.0.0", + "esbuild-sass-plugin": "^3.6.0", + "github-slugger": "^2.0.0", + "globby": "^16.1.0", + "hast-util-to-jsx-runtime": "^2.3.6", + "isomorphic-git": "^1.36.3", + "lightningcss": "^1.31.1", + "micromorph": "^0.4.5", + "minimatch": "^10.1.1", + "preact": "^10.28.2", + "preact-render-to-string": "^6.6.5", + "pretty-bytes": "^7.1.0", + "pretty-time": "^1.1.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "rfdc": "^1.4.1", + "serve-handler": "^6.1.6", + "sharp": "^0.34.5", + "source-map-support": "^0.5.21", + "to-vfile": "^8.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3", + "workerpool": "^10.0.1", + "ws": "^8.19.0", + "yaml": "^2.8.2", + "yargs": "^18.0.0" + }, + "devDependencies": { + "@quartz-community/types": "github:quartz-community/types", + "@quartz-community/utils": "github:quartz-community/utils", + "@types/hast": "^3.0.4", + "@types/node": "^25.0.10", + "@types/pretty-time": "^1.1.5", + "@types/source-map-support": "^0.5.10", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.35", + "esbuild": "^0.27.2", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/Local/storage/thlab-notes/worker/quartz.config.default.yaml b/Local/storage/thlab-notes/worker/quartz.config.default.yaml new file mode 100644 index 0000000..209101d --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz.config.default.yaml @@ -0,0 +1,280 @@ +# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json +configuration: + pageTitle: Quartz 5 + pageTitleSuffix: "" + enableSPA: true + enablePopovers: true + analytics: + provider: plausible + locale: en-US + baseUrl: tinyhome.ndmv.net + ignorePatterns: + - private + - templates + - .obsidian + theme: + fontOrigin: googleFonts + cdnCaching: true + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono + colors: + lightMode: + light: "#faf8f8" + lightgray: "#e5e5e5" + gray: "#b8b8b8" + darkgray: "#4e4e4e" + dark: "#2b2b2b" + secondary: "#284b63" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#fff23688" + darkMode: + light: "#161618" + lightgray: "#393639" + gray: "#646464" + darkgray: "#d4d4d4" + dark: "#ebebec" + secondary: "#7b97aa" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#b3aa0288" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + options: + enableInHtmlEmbed: false + enableCheckbox: true + order: 30 + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: github:quartz-community/crawl-links + enabled: true + options: + markdownLinkResolution: shortest + order: 60 + - source: github:quartz-community/description + enabled: true + order: 70 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: github:quartz-community/citations + enabled: false + order: 85 + - source: github:quartz-community/hard-line-breaks + enabled: false + order: 90 + - source: github:quartz-community/ox-hugo + enabled: false + order: 91 + - source: github:quartz-community/roam + enabled: false + order: 92 + - source: github:quartz-community/fonts + enabled: true + - source: github:quartz-community/remove-draft + enabled: true + - source: github:quartz-community/explicit-publish + enabled: false + - source: github:quartz-community/alias-redirects + enabled: true + - source: github:quartz-community/content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: github:quartz-community/favicon + enabled: true + - source: github:quartz-community/og-image + enabled: true + - source: github:quartz-community/cname + enabled: true + - source: github:quartz-community/canvas-page + enabled: true + - source: github:quartz-community/content-page + enabled: true + - source: github:quartz-community/folder-page + enabled: true + - source: github:quartz-community/tag-page + enabled: true + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 50 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/page-title + enabled: true + layout: + position: left + priority: 10 + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: github:quartz-community/comments + enabled: false + options: + provider: giscus + options: {} + layout: + position: afterBody + priority: 10 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: github:quartz-community/recent-notes + enabled: false + - source: github:quartz-community/spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: github:quartz-community/bases-page + enabled: true + options: {} + order: 50 + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: --- + language: yaml + order: 5 + layout: + position: beforeBody + priority: 15 + display: all + - source: github:quartz-community/unlisted-pages + enabled: true + options: {} + order: 45 + - source: github:quartz-community/encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json + order: 900 + layout: + position: body + priority: 100 + display: all + - source: github:quartz-community/stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + display: all +layout: + groups: + toolbar: + priority: 35 + direction: row + gap: 0.5rem + byPageType: + "404": + positions: + beforeBody: [] + left: [] + right: [] + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + canvas: {} + bases: {} diff --git a/Local/storage/thlab-notes/worker/quartz.lock.json b/Local/storage/thlab-notes/worker/quartz.lock.json new file mode 100644 index 0000000..3f97ed5 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz.lock.json @@ -0,0 +1,286 @@ +{ + "version": "1.0.0", + "plugins": { + "alias-redirects": { + "source": "github:quartz-community/alias-redirects", + "resolved": "https://github.com/quartz-community/alias-redirects.git", + "commit": "73a98dda7e4f55239310833299d91daf8611349f", + "installedAt": "2026-06-09T17:57:45.809Z" + }, + "article-title": { + "source": "github:quartz-community/article-title", + "resolved": "https://github.com/quartz-community/article-title.git", + "commit": "e608ca815e137e22b598094f735bcd8a481dafaa", + "installedAt": "2026-06-03T13:32:35.204Z" + }, + "backlinks": { + "source": "github:quartz-community/backlinks", + "resolved": "https://github.com/quartz-community/backlinks.git", + "commit": "7490f921b7bd974c3f2f985ad3744b06160827d6", + "installedAt": "2026-06-03T13:32:35.225Z" + }, + "bases-page": { + "source": "github:quartz-community/bases-page", + "resolved": "https://github.com/quartz-community/bases-page.git", + "commit": "f8fc7da3515a4d4bbe0bd4be0fd9188823866a01", + "installedAt": "2026-06-03T13:32:35.344Z" + }, + "breadcrumbs": { + "source": "github:quartz-community/breadcrumbs", + "resolved": "https://github.com/quartz-community/breadcrumbs.git", + "commit": "cf2e161425165e1ac713f1feb7250b07fe0250ae", + "installedAt": "2026-06-03T13:32:35.270Z" + }, + "canvas-page": { + "source": "github:quartz-community/canvas-page", + "resolved": "https://github.com/quartz-community/canvas-page.git", + "commit": "84fc96799671a65f76b66e30217dbefdd43a2905", + "installedAt": "2026-06-03T13:32:35.291Z" + }, + "citations": { + "source": "github:quartz-community/citations", + "resolved": "https://github.com/quartz-community/citations.git", + "commit": "2ad133a123c5b15b7c41d4fb684d422622113081", + "installedAt": "2026-06-03T13:16:37.060Z" + }, + "cname": { + "source": "github:quartz-community/cname", + "resolved": "https://github.com/quartz-community/cname.git", + "commit": "ec4c81c91da13a6f6c2cbe1656daf5c14575dab3", + "installedAt": "2026-06-03T13:16:37.301Z" + }, + "comments": { + "source": "github:quartz-community/comments", + "resolved": "https://github.com/quartz-community/comments.git", + "commit": "42c5023e42cf62495219095a862b2ea144b65600", + "installedAt": "2026-06-03T13:32:35.236Z" + }, + "content-index": { + "source": "github:quartz-community/content-index", + "resolved": "https://github.com/quartz-community/content-index.git", + "commit": "c3d4f5c85311712c3355cd71da46b28e2d8eba71", + "installedAt": "2026-06-03T13:32:35.431Z" + }, + "content-meta": { + "source": "github:quartz-community/content-meta", + "resolved": "https://github.com/quartz-community/content-meta.git", + "commit": "dd6e94b5ca1cb195104a2b5e624a43ee6aa0a324", + "installedAt": "2026-06-03T13:32:35.221Z" + }, + "content-page": { + "source": "github:quartz-community/content-page", + "resolved": "https://github.com/quartz-community/content-page.git", + "commit": "d22fae357ae74a3e97a2f450862f23f5227842c4", + "installedAt": "2026-06-03T13:32:35.505Z" + }, + "crawl-links": { + "source": "github:quartz-community/crawl-links", + "resolved": "https://github.com/quartz-community/crawl-links.git", + "commit": "43edc6d5182e79bf1b63fed7eb3ba0c7624a1526", + "installedAt": "2026-06-03T13:32:35.242Z" + }, + "created-modified-date": { + "source": "github:quartz-community/created-modified-date", + "resolved": "https://github.com/quartz-community/created-modified-date.git", + "commit": "c003199fb842969d43ee9e0f54120a85e588260e", + "installedAt": "2026-06-03T13:16:36.740Z" + }, + "darkmode": { + "source": "github:quartz-community/darkmode", + "resolved": "https://github.com/quartz-community/darkmode.git", + "commit": "c6484f72ebc6ea89339be7cf86ad14b40c47dcc7", + "installedAt": "2026-06-03T13:32:35.196Z" + }, + "description": { + "source": "github:quartz-community/description", + "resolved": "https://github.com/quartz-community/description.git", + "commit": "56dc546614d905ad07dd0da8dd5820e25e5ea97b", + "installedAt": "2026-06-03T13:32:35.233Z" + }, + "encrypted-pages": { + "source": "github:quartz-community/encrypted-pages", + "resolved": "https://github.com/quartz-community/encrypted-pages.git", + "commit": "89e88af565dfaa9ab2d9c99677b82394ea7be356", + "installedAt": "2026-06-03T13:32:35.661Z" + }, + "explicit-publish": { + "source": "github:quartz-community/explicit-publish", + "resolved": "https://github.com/quartz-community/explicit-publish.git", + "commit": "4afb630a1cc9034267beb93c928e81d0fa2f4d4b", + "installedAt": "2026-06-03T13:16:36.746Z" + }, + "explorer": { + "source": "github:quartz-community/explorer", + "resolved": "https://github.com/quartz-community/explorer.git", + "commit": "a2dfd1373abe58ace461ebea0b4e94cb287f894e", + "installedAt": "2026-06-03T13:32:35.808Z" + }, + "favicon": { + "source": "github:quartz-community/favicon", + "resolved": "https://github.com/quartz-community/favicon.git", + "commit": "85842d5c15f937a3d1a02c45accee27118146d73", + "installedAt": "2026-06-03T13:16:37.198Z" + }, + "folder-page": { + "source": "github:quartz-community/folder-page", + "resolved": "https://github.com/quartz-community/folder-page.git", + "commit": "93304d22e1d7f09f93a33658ec273f7cb8d17793", + "installedAt": "2026-06-03T13:32:35.810Z" + }, + "fonts": { + "source": "github:quartz-community/fonts", + "resolved": "https://github.com/quartz-community/fonts.git", + "commit": "8be5be706d19ca34fa8e342327b78e11aff3d4f0", + "installedAt": "2026-06-10T21:13:18.081Z" + }, + "footer": { + "source": "github:quartz-community/footer", + "resolved": "https://github.com/quartz-community/footer.git", + "commit": "6ed61928d3c0178d7cef972ebcbca6a206a2f065", + "installedAt": "2026-06-03T13:32:35.802Z" + }, + "github-flavored-markdown": { + "source": "github:quartz-community/github-flavored-markdown", + "resolved": "https://github.com/quartz-community/github-flavored-markdown.git", + "commit": "3eabbaa252ce175665ab3f62e1af25948a83e8b6", + "installedAt": "2026-06-03T13:16:36.665Z" + }, + "graph": { + "source": "github:quartz-community/graph", + "resolved": "https://github.com/quartz-community/graph.git", + "commit": "46f0ba1c3c0cc484697572e7bcf315fa384d80d2", + "installedAt": "2026-06-03T13:32:35.872Z" + }, + "hard-line-breaks": { + "source": "github:quartz-community/hard-line-breaks", + "resolved": "https://github.com/quartz-community/hard-line-breaks.git", + "commit": "0d448f38a24c568c5539ff2cd57d813293430ccd", + "installedAt": "2026-06-03T13:16:36.965Z" + }, + "latex": { + "source": "github:quartz-community/latex", + "resolved": "https://github.com/quartz-community/latex.git", + "commit": "3dcfedcb5ae5e28b71248a72665ccb881e82c185", + "installedAt": "2026-06-03T13:16:36.841Z" + }, + "note-properties": { + "source": "github:quartz-community/note-properties", + "resolved": "https://github.com/quartz-community/note-properties.git", + "commit": "3cb40141e792a8a9ba9f99553cd436f36411bf8d", + "installedAt": "2026-06-03T13:32:35.812Z" + }, + "obsidian-flavored-markdown": { + "source": "github:quartz-community/obsidian-flavored-markdown", + "resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git", + "commit": "07eaca7b31a537c7c4a0fd2848b1f00014c940af", + "installedAt": "2026-06-03T13:32:35.815Z" + }, + "obsidian-plugin-excalidraw": { + "source": "github:quartz-community/obsidian-plugin-excalidraw", + "resolved": "https://github.com/quartz-community/obsidian-plugin-excalidraw.git", + "commit": "dad0a4c0abdc8b3c2786277a2cbd67dc039c754c", + "installedAt": "2026-06-03T13:32:35.851Z" + }, + "og-image": { + "source": "github:quartz-community/og-image", + "resolved": "https://github.com/quartz-community/og-image.git", + "commit": "31343c612d02c5fd22ff27a1e6035b2486be75f5", + "installedAt": "2026-06-03T13:32:35.899Z" + }, + "ox-hugo": { + "source": "github:quartz-community/ox-hugo", + "resolved": "https://github.com/quartz-community/ox-hugo.git", + "commit": "832871da9ab33d52f904456c1a5e14f1b2057d83", + "installedAt": "2026-06-03T13:16:36.714Z" + }, + "page-title": { + "source": "github:quartz-community/page-title", + "resolved": "https://github.com/quartz-community/page-title.git", + "commit": "a1c1fe0a9c6a5ce1acf6efa01d473a7d9850e2a3", + "installedAt": "2026-06-03T13:32:36.002Z" + }, + "quartz-themes": { + "source": { + "name": "quartz-themes", + "repo": "github:saberzero1/quartz-themes", + "subdir": "plugin" + }, + "resolved": "https://github.com/saberzero1/quartz-themes.git", + "commit": "945135e8b70576753d0591e1713fe56553100b3d", + "subdir": "plugin", + "installedAt": "2026-06-11T15:50:25.576Z" + }, + "reader-mode": { + "source": "github:quartz-community/reader-mode", + "resolved": "https://github.com/quartz-community/reader-mode.git", + "commit": "73c8bce66df8835510ffac47e53b1ca0f7efd51e", + "installedAt": "2026-06-03T13:32:36.320Z" + }, + "recent-notes": { + "source": "github:quartz-community/recent-notes", + "resolved": "https://github.com/quartz-community/recent-notes.git", + "commit": "3c3d104335e2fc5ff62371ead157667adb79b046", + "installedAt": "2026-06-03T13:32:36.284Z" + }, + "remove-draft": { + "source": "github:quartz-community/remove-draft", + "resolved": "https://github.com/quartz-community/remove-draft.git", + "commit": "0a51a4c6abb3c86961a9bbf46f4538da87fcf9f8", + "installedAt": "2026-06-03T13:16:36.691Z" + }, + "roam": { + "source": "github:quartz-community/roam", + "resolved": "https://github.com/quartz-community/roam.git", + "commit": "b2aad70fdf548d07b8fd28df9edbdba7f256f166", + "installedAt": "2026-06-03T13:16:36.771Z" + }, + "search": { + "source": "github:quartz-community/search", + "resolved": "https://github.com/quartz-community/search.git", + "commit": "0f4c1a233cd03a0f562e13636b89b7708f8e2698", + "installedAt": "2026-06-03T13:32:36.597Z" + }, + "spacer": { + "source": "github:quartz-community/spacer", + "resolved": "https://github.com/quartz-community/spacer.git", + "commit": "64b135dcfb78c4911f8c42d942872d645cd2a456", + "installedAt": "2026-06-03T13:16:37.913Z" + }, + "stacked-pages": { + "source": "github:quartz-community/stacked-pages", + "resolved": "https://github.com/quartz-community/stacked-pages.git", + "commit": "e88fe063c68f74e6ba2dc7c54ddb353bb10c7982", + "installedAt": "2026-06-03T13:32:36.330Z" + }, + "syntax-highlighting": { + "source": "github:quartz-community/syntax-highlighting", + "resolved": "https://github.com/quartz-community/syntax-highlighting.git", + "commit": "5bfdc2c3f42d3d0326c4e777eb575f3fb68d51fb", + "installedAt": "2026-06-03T13:16:37.737Z" + }, + "table-of-contents": { + "source": "github:quartz-community/table-of-contents", + "resolved": "https://github.com/quartz-community/table-of-contents.git", + "commit": "6984305e5dae0830c025450e160f12610406f7a4", + "installedAt": "2026-06-03T13:32:36.265Z" + }, + "tag-list": { + "source": "github:quartz-community/tag-list", + "resolved": "https://github.com/quartz-community/tag-list.git", + "commit": "c48324e2ba768e2a0a5c4c17cd0661b190d075c5", + "installedAt": "2026-06-03T13:32:36.346Z" + }, + "tag-page": { + "source": "github:quartz-community/tag-page", + "resolved": "https://github.com/quartz-community/tag-page.git", + "commit": "eabf3eec55c0bab7f49e69957341f34d2ae3c5a1", + "installedAt": "2026-06-03T13:32:36.377Z" + }, + "unlisted-pages": { + "source": "github:quartz-community/unlisted-pages", + "resolved": "https://github.com/quartz-community/unlisted-pages.git", + "commit": "f512d8672862c5891e63d755ba04b66277e6d760", + "installedAt": "2026-06-03T13:32:36.647Z" + } + } +} diff --git a/Local/storage/thlab-notes/worker/quartz.ts b/Local/storage/thlab-notes/worker/quartz.ts new file mode 100644 index 0000000..7a257d1 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz.ts @@ -0,0 +1,5 @@ +import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader" + +const config = await loadQuartzConfig() +export default config +export const layout = await loadQuartzLayout() diff --git a/Local/storage/thlab-notes/worker/quartz/bootstrap-cli.mjs b/Local/storage/thlab-notes/worker/quartz/bootstrap-cli.mjs new file mode 100755 index 0000000..34288c7 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/bootstrap-cli.mjs @@ -0,0 +1,284 @@ +#!/usr/bin/env -S node --no-deprecation +const [major] = process.versions.node.split(".").map(Number) +if (major < 22) { + console.error( + `\nQuartz requires Node.js >= 22, but you are running Node.js ${process.version}.\n` + + `Please upgrade: https://nodejs.org/\n`, + ) + process.exit(1) +} +import yargs from "yargs" +import { hideBin } from "yargs/helpers" +import { + handleBuild, + handleCreate, + handleUpgrade, + handleRestore, + handleSync, +} from "./cli/handlers.js" + +import { + handlePluginInstallUnified, + handlePluginAdd, + handlePluginRemove, + handlePluginList, + handlePluginStatus, + handlePluginEnable, + handlePluginDisable, + handlePluginConfig, + handlePluginPrune, +} from "./cli/plugin-git-handlers.js" +import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js" +import { version } from "./cli/constants.js" + +async function launchTui() { + const { join } = await import("path") + const { existsSync } = await import("fs") + const { spawn } = await import("child_process") + const tuiPath = join(process.cwd(), ".quartz", "plugins", "tui", "dist", "App.mjs") + + if (!existsSync(tuiPath)) { + console.error( + "TUI plugin not installed. Install with:\n" + + " npx quartz plugin add github:quartz-community/tui\n", + ) + process.exit(1) + } + + // OpenTUI requires Bun runtime (uses bun:ffi for Zig renderer) + return new Promise((resolve, reject) => { + const child = spawn("bun", ["run", tuiPath], { + stdio: "inherit", + cwd: process.cwd(), + }) + + child.on("error", (err) => { + if (err.code === "ENOENT") { + console.error( + "Error: Bun runtime not found. The TUI requires Bun to run.\n" + + "Install Bun: https://bun.sh/docs/installation", + ) + } + reject(err) + }) + + child.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`TUI exited with code ${code}`)) + } + }) + }) +} + +yargs(hideBin(process.argv)) + .scriptName("quartz") + .version(version) + .usage("$0 [args]") + .command("create", "Initialize Quartz", CreateArgv, async (argv) => { + await handleCreate(argv) + }) + .command( + ["upgrade", "update"], + "Upgrade Quartz to the latest version", + CommonArgv, + async (argv) => { + await handleUpgrade(argv) + }, + ) + .command( + "restore", + "Try to restore your content folder from the cache", + CommonArgv, + async (argv) => { + await handleRestore(argv) + }, + ) + .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => { + await handleSync(argv) + }) + .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => { + await handleBuild(argv) + }) + .command("tui", "Launch interactive plugin manager", CommonArgv, async () => { + await launchTui() + }) + .command( + "plugin [subcommand]", + "Manage Quartz plugins", + (yargs) => { + return ( + yargs + .command( + "install [names..]", + "Install plugins from lockfile or config", + { + ...CommonArgv, + "from-config": { + boolean: true, + default: false, + describe: "install plugins referenced in quartz.config.yaml instead of lockfile", + }, + latest: { + boolean: true, + default: false, + describe: "fetch latest version from remote instead of pinned lockfile commit", + }, + clean: { + boolean: true, + default: false, + describe: "skip plugins whose directory already exists", + }, + "dry-run": { + boolean: true, + default: false, + describe: "show what would happen without making changes", + }, + }, + async (argv) => { + await handlePluginInstallUnified({ + names: argv.names?.length ? argv.names : undefined, + fromConfig: argv.fromConfig, + latest: argv.latest, + clean: argv.clean, + dryRun: argv.dryRun, + concurrency: argv.concurrency, + }) + }, + ) + .command( + "add ", + "Add plugins from Git repositories", + { + ...CommonArgv, + name: { + string: true, + alias: ["as"], + describe: "Override the plugin name (for resolving conflicts with duplicate names)", + }, + subdir: { + string: true, + describe: "Subdirectory within the repository containing the plugin", + }, + }, + async (argv) => { + await handlePluginAdd(argv.repos, { + name: argv.name, + subdir: argv.subdir, + concurrency: argv.concurrency, + }) + }, + ) + .command("remove ", "Remove installed plugins", CommonArgv, async (argv) => { + await handlePluginRemove(argv.names) + }) + .command("list", "List all installed plugins", CommonArgv, async () => { + await handlePluginList() + }) + .command( + "enable ", + "Enable plugins in quartz.config.yaml", + CommonArgv, + async (argv) => { + await handlePluginEnable(argv.names) + }, + ) + .command( + "disable ", + "Disable plugins in quartz.config.yaml", + CommonArgv, + async (argv) => { + await handlePluginDisable(argv.names) + }, + ) + .command( + "config ", + "View or set plugin configuration", + { + ...CommonArgv, + set: { + string: true, + describe: "Set a config value (key=value)", + }, + }, + async (argv) => { + await handlePluginConfig(argv.name, { set: argv.set }) + }, + ) + .command( + "prune", + "Remove installed plugins no longer referenced in config", + { + ...CommonArgv, + "dry-run": { + boolean: true, + default: false, + describe: "show what would be pruned without making changes", + }, + }, + async (argv) => { + await handlePluginPrune({ dryRun: argv.dryRun }) + }, + ) + // Hidden deprecated aliases + .command("restore", false, CommonArgv, async (argv) => { + console.log( + "\x1b[33m⚠ 'plugin restore' is deprecated. Use 'plugin install --clean' instead.\x1b[0m", + ) + await handlePluginInstallUnified({ clean: true, concurrency: argv.concurrency }) + }) + .command("update [names..]", false, CommonArgv, async (argv) => { + console.log( + "\x1b[33m⚠ 'plugin update' is deprecated. Use 'plugin install --latest' instead.\x1b[0m", + ) + await handlePluginInstallUnified({ + names: argv.names?.length ? argv.names : undefined, + latest: true, + concurrency: argv.concurrency, + }) + }) + .command("check", false, CommonArgv, async (argv) => { + console.log( + "\x1b[33m⚠ 'plugin check' is deprecated. Use 'plugin install --latest --dry-run' instead.\x1b[0m", + ) + await handlePluginInstallUnified({ + latest: true, + dryRun: true, + concurrency: argv.concurrency, + }) + }) + .command( + "resolve", + false, + { + ...CommonArgv, + "dry-run": { + boolean: true, + default: false, + describe: "show what would be resolved without making changes", + }, + }, + async (argv) => { + console.log( + "\x1b[33m⚠ 'plugin resolve' is deprecated. Use 'plugin install --from-config' instead.\x1b[0m", + ) + await handlePluginInstallUnified({ + fromConfig: true, + dryRun: argv.dryRun, + concurrency: argv.concurrency, + }) + }, + ) + .demandCommand(0, "") + ) + }, + async (argv) => { + if (!argv._.includes("plugin") || argv._.length > 1) return + await handlePluginStatus() + }, + ) + .showHelpOnFail(true) + .help() + .strict() + .demandCommand().argv diff --git a/Local/storage/thlab-notes/worker/quartz/bootstrap-worker.mjs b/Local/storage/thlab-notes/worker/quartz/bootstrap-worker.mjs new file mode 100644 index 0000000..c4c4949 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/bootstrap-worker.mjs @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import workerpool from "workerpool" +const cacheFile = "./.quartz-cache/transpiled-worker.mjs" +const { parseMarkdown, processHtml } = await import(cacheFile) +workerpool.worker({ + parseMarkdown, + processHtml, +}) diff --git a/Local/storage/thlab-notes/worker/quartz/build.ts b/Local/storage/thlab-notes/worker/quartz/build.ts new file mode 100644 index 0000000..3d73641 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/build.ts @@ -0,0 +1,369 @@ +import sourceMapSupport from "source-map-support" +sourceMapSupport.install(options) +import path from "path" +import { PerfTimer } from "./util/perf" +import { rm } from "fs/promises" +import { GlobbyFilterFunction, isGitIgnored } from "globby" +import { styleText } from "util" +import { parseMarkdown } from "./processors/parse" +import { filterContent } from "./processors/filter" +import { emitContent } from "./processors/emit" +import cfg from "../quartz" +import { FilePath, joinSegments, slugifyFilePath } from "./util/path" +import { detectSlugCollisions, formatCollisionWarning } from "./util/slugCollisions" +import chokidar from "chokidar" +import { ProcessedContent } from "./plugins/vfile" +import { Argv, BuildCtx } from "./util/ctx" +import { glob, toPosixPath } from "./util/glob" +import { trace } from "./util/trace" +import { options } from "./util/sourcemap" +import { Mutex } from "async-mutex" +import { getStaticResourcesFromPlugins } from "./plugins" +import { randomIdNonSecure } from "./util/random" +import { ChangeEvent } from "./plugins/types" +import { minimatch } from "minimatch" + +function reportSlugCollisions(content: ProcessedContent[]): void { + const collisions = detectSlugCollisions(content) + if (collisions.length === 0) return + console.warn(styleText("yellow", formatCollisionWarning(collisions))) +} + +type ContentMap = Map< + FilePath, + | { + type: "markdown" + content: ProcessedContent + } + | { + type: "other" + } +> + +type BuildData = { + ctx: BuildCtx + ignored: GlobbyFilterFunction + mut: Mutex + contentMap: ContentMap + changesSinceLastBuild: Record + lastBuildMs: number +} + +async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { + const ctx: BuildCtx = { + buildId: randomIdNonSecure(), + argv, + cfg, + allSlugs: [], + allFiles: [], + incremental: false, + virtualPages: [], + } + + const perf = new PerfTimer() + const output = argv.output + + const pluginCount = Object.values(cfg.plugins).flat().length + const pluginNames = (key: "transformers" | "filters" | "emitters" | "pageTypes") => + (cfg.plugins[key] ?? []).map((plugin) => plugin.name) + if (argv.verbose) { + console.log(`Loaded ${pluginCount} plugins`) + console.log(` Transformers: ${pluginNames("transformers").join(", ")}`) + console.log(` Filters: ${pluginNames("filters").join(", ")}`) + console.log(` Emitters: ${pluginNames("emitters").join(", ")}`) + console.log(` PageTypes: ${pluginNames("pageTypes").join(", ")}`) + } + + const release = await mut.acquire() + perf.addEvent("clean") + await rm(output, { recursive: true, force: true }) + console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) + + perf.addEvent("glob") + const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns) + const markdownPaths = allFiles.filter((fp) => fp.endsWith(".md")).sort() + console.log( + `Found ${markdownPaths.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, + ) + + const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath) + ctx.allFiles = allFiles + ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath)) + + const parsedFiles = await parseMarkdown(ctx, filePaths) + reportSlugCollisions(parsedFiles) + const filteredContent = filterContent(ctx, parsedFiles) + + await emitContent(ctx, filteredContent) + console.log( + styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`), + ) + release() + + if (argv.watch) { + ctx.incremental = true + return startWatching(ctx, mut, parsedFiles, clientRefresh) + } +} + +// setup watcher for rebuilds +async function startWatching( + ctx: BuildCtx, + mut: Mutex, + initialContent: ProcessedContent[], + clientRefresh: () => void, +) { + const { argv, allFiles } = ctx + + const contentMap: ContentMap = new Map() + for (const filePath of allFiles) { + contentMap.set(filePath, { + type: "other", + }) + } + + for (const content of initialContent) { + const [_tree, vfile] = content + const relPath = vfile.data.relativePath + if (!relPath) { + console.warn(`Skipping file with no relativePath: ${vfile.path}`) + continue + } + contentMap.set(relPath, { + type: "markdown", + content, + }) + } + + const gitIgnoredMatcher = await isGitIgnored() + const buildData: BuildData = { + ctx, + mut, + contentMap, + ignored: (fp) => { + const pathStr = toPosixPath(fp.toString()) + if (pathStr.startsWith(".git/")) return true + if (gitIgnoredMatcher(pathStr)) return true + for (const pattern of cfg.configuration.ignorePatterns) { + if (minimatch(pathStr, pattern)) { + return true + } + } + + return false + }, + + changesSinceLastBuild: {}, + lastBuildMs: 0, + } + + const watcher = chokidar.watch(".", { + awaitWriteFinish: { stabilityThreshold: 250 }, + persistent: true, + cwd: argv.directory, + ignoreInitial: true, + }) + + const changes: ChangeEvent[] = [] + let rebuildTimeout: ReturnType | null = null + const scheduleRebuild = () => { + if (rebuildTimeout) clearTimeout(rebuildTimeout) + rebuildTimeout = setTimeout(() => { + rebuildTimeout = null + rebuild(changes, clientRefresh, buildData).catch((err) => { + console.error(styleText("red", "Rebuild failed:"), err.message ?? err) + }) + }, 100) + } + watcher + .on("add", (fp) => { + fp = toPosixPath(fp) + if (buildData.ignored(fp)) return + changes.push({ path: fp as FilePath, type: "add" }) + scheduleRebuild() + }) + .on("change", (fp) => { + fp = toPosixPath(fp) + if (buildData.ignored(fp)) return + changes.push({ path: fp as FilePath, type: "change" }) + scheduleRebuild() + }) + .on("unlink", (fp) => { + fp = toPosixPath(fp) + if (buildData.ignored(fp)) return + changes.push({ path: fp as FilePath, type: "delete" }) + scheduleRebuild() + }) + + return async () => { + await watcher.close() + } +} + +async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildData: BuildData) { + const { ctx, contentMap, mut, changesSinceLastBuild } = buildData + const { argv, cfg } = ctx + + const buildId = randomIdNonSecure() + ctx.buildId = buildId + buildData.lastBuildMs = new Date().getTime() + const numChangesInBuild = changes.length + const release = await mut.acquire() + try { + // if there's another build after us, release and let them do it + if (ctx.buildId !== buildId) { + return + } + + const perf = new PerfTimer() + perf.addEvent("rebuild") + console.log(styleText("yellow", "Detected change, rebuilding...")) + + // update changesSinceLastBuild + for (const change of changes) { + changesSinceLastBuild[change.path] = change.type + } + + const staticResources = getStaticResourcesFromPlugins(ctx) + const pathsToParse: FilePath[] = [] + for (const [fp, type] of Object.entries(changesSinceLastBuild)) { + if (type === "delete" || path.extname(fp) !== ".md") continue + const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath + pathsToParse.push(fullPath) + } + + const parsed = await parseMarkdown(ctx, pathsToParse) + for (const content of parsed) { + const relPath = content[1].data.relativePath + if (!relPath) { + console.warn(`Skipping file with no relativePath: ${content[1].path}`) + continue + } + contentMap.set(relPath, { + type: "markdown", + content, + }) + } + + // update state using changesSinceLastBuild + // we do this weird play of add => compute change events => remove + // so that partialEmitters can do appropriate cleanup based on the content of deleted files + for (const [file, change] of Object.entries(changesSinceLastBuild)) { + if (change === "delete") { + // universal delete case + contentMap.delete(file as FilePath) + } + + // manually track non-markdown files as processed files only + // contains markdown files + if (change === "add" && path.extname(file) !== ".md") { + contentMap.set(file as FilePath, { + type: "other", + }) + } + } + + const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => { + const path = fp as FilePath + const processedContent = contentMap.get(path) + if (processedContent?.type === "markdown") { + const [_tree, file] = processedContent.content + return { + type, + path, + file, + } + } + + return { + type, + path, + } + }) + + // update allFiles and then allSlugs with the consistent view of content map + ctx.allFiles = Array.from(contentMap.keys()) + ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath)) + + const markdownContent = Array.from(contentMap.values()) + .filter((file) => file.type === "markdown") + .map((file) => file.content) + reportSlugCollisions(markdownContent) + let processedFiles = filterContent(ctx, markdownContent) + + let emittedFiles = 0 + + // Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages + const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher") + if (dispatcher) { + ctx.virtualPages = [] + const emitFn = dispatcher.partialEmit ?? dispatcher.emit + const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents) + if (emitted !== null) { + if (Symbol.asyncIterator in emitted) { + for await (const file of emitted) { + emittedFiles++ + if (ctx.argv.verbose) { + console.log(`[emit:${dispatcher.name}] ${file}`) + } + } + } else { + emittedFiles += emitted.length + if (ctx.argv.verbose) { + for (const file of emitted) { + console.log(`[emit:${dispatcher.name}] ${file}`) + } + } + } + } + } + + // Phase 2: Run all other emitters with content extended by virtual pages + const contentWithVirtual = + ctx.virtualPages.length > 0 ? [...processedFiles, ...ctx.virtualPages] : processedFiles + for (const emitter of cfg.plugins.emitters) { + if (emitter.name === "PageTypeDispatcher") continue + // Try to use partialEmit if available, otherwise assume the output is static + const emitFn = emitter.partialEmit ?? emitter.emit + const emitted = await emitFn(ctx, contentWithVirtual, staticResources, changeEvents) + if (emitted === null) { + continue + } + + if (Symbol.asyncIterator in emitted) { + // Async generator case + for await (const file of emitted) { + emittedFiles++ + if (ctx.argv.verbose) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + } else { + // Array case + emittedFiles += emitted.length + if (ctx.argv.verbose) { + for (const file of emitted) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + } + } + + console.log( + `Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`, + ) + console.log(styleText("green", `Done rebuilding in ${perf.timeSince()}`)) + changes.splice(0, numChangesInBuild) + clientRefresh() + } finally { + release() + } +} + +export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { + try { + return await buildQuartz(argv, mut, clientRefresh) + } catch (err) { + trace("\nExiting Quartz due to a fatal error", err as Error) + } +} diff --git a/Local/storage/thlab-notes/worker/quartz/cfg.ts b/Local/storage/thlab-notes/worker/quartz/cfg.ts new file mode 100644 index 0000000..9027c46 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cfg.ts @@ -0,0 +1,106 @@ +import { QuartzComponent } from "./components/types" +import { ValidLocale } from "./i18n" +import { PluginSpecifier } from "./plugins/loader/types" +import { PluginTypes } from "./plugins/types" +import { Theme } from "./util/theme" + +export type Analytics = + | null + | { + provider: "plausible" + host?: string + } + | { + provider: "google" + tagId: string + } + | { + provider: "umami" + websiteId: string + host?: string + } + | { + provider: "goatcounter" + websiteId: string + host?: string + scriptSrc?: string + } + | { + provider: "posthog" + apiKey: string + host?: string + } + | { + provider: "tinylytics" + siteId: string + } + | { + provider: "cabin" + host?: string + } + | { + provider: "clarity" + projectId?: string + } + | { + provider: "matomo" + host: string + siteId: string + } + | { + provider: "vercel" + } + | { + provider: "rybbit" + siteId: string + host?: string + } + +export interface GlobalConfiguration { + pageTitle: string + pageTitleSuffix?: string + /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ + enableSPA: boolean + /** Whether to display Wikipedia-style popovers when hovering over links */ + enablePopovers: boolean + /** Analytics mode */ + analytics: Analytics + /** Glob patterns to not search */ + ignorePatterns: string[] + /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. + * Quartz will avoid using this as much as possible and use relative URLs most of the time + */ + baseUrl?: string + theme: Theme + /** + * Allow to translate the date in the language of your choice. + * Also used for UI translation (default: en-US) + * Need to be formatted following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag + * The first part is the language (en) and the second part is the script/region (US) + * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes + * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + */ + locale: ValidLocale +} + +export interface QuartzConfig { + configuration: GlobalConfiguration + plugins: PluginTypes + externalPlugins?: PluginSpecifier[] +} + +export interface FullPageLayout { + head: QuartzComponent + header: QuartzComponent[] + beforeBody: QuartzComponent[] + pageBody: QuartzComponent + afterBody: QuartzComponent[] + left: QuartzComponent[] + right: QuartzComponent[] + footer: QuartzComponent + /** Page frame name (e.g. "default", "full-width", "minimal"). Defaults to "default". */ + frame?: string +} + +export type PageLayout = Pick +export type SharedLayout = Pick diff --git a/Local/storage/thlab-notes/worker/quartz/cli/args.js b/Local/storage/thlab-notes/worker/quartz/cli/args.js new file mode 100644 index 0000000..7a6c155 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/args.js @@ -0,0 +1,120 @@ +export const CommonArgv = { + directory: { + string: true, + alias: ["d"], + default: "content", + describe: "directory to look for content files", + }, + verbose: { + boolean: true, + alias: ["v"], + default: false, + describe: "print out extra logging information", + }, + concurrency: { + number: true, + alias: ["c"], + describe: "max parallel operations (default: number of CPU cores)", + }, +} + +export const CreateArgv = { + ...CommonArgv, + template: { + string: true, + alias: ["t"], + choices: ["default", "obsidian", "ttrpg", "blog"], + describe: "template to use for initial configuration", + }, + source: { + string: true, + alias: ["s"], + describe: "source directory to copy/create symlink from", + }, + strategy: { + string: true, + alias: ["X"], + choices: ["new", "copy", "symlink"], + describe: "strategy for content folder setup", + }, + baseUrl: { + string: true, + alias: ["b"], + describe: "base URL for your Quartz site (e.g. mysite.github.io/quartz)", + }, + links: { + string: true, + alias: ["l"], + choices: ["absolute", "shortest", "relative"], + describe: "strategy to resolve links", + }, +} + +export const SyncArgv = { + ...CommonArgv, + commit: { + boolean: true, + default: true, + describe: "create a git commit for your unsaved changes", + }, + message: { + string: true, + alias: ["m"], + describe: "option to override the default Quartz commit message", + }, + push: { + boolean: true, + default: true, + describe: "push updates to your Quartz fork", + }, + pull: { + boolean: true, + default: true, + describe: "pull updates from your Quartz fork", + }, +} + +export const BuildArgv = { + ...CommonArgv, + output: { + string: true, + alias: ["o"], + default: "public", + describe: "output folder for files", + }, + serve: { + boolean: true, + default: false, + describe: "run a local server to live-preview your Quartz", + }, + watch: { + boolean: true, + default: false, + describe: "watch for changes and rebuild automatically", + }, + baseDir: { + string: true, + default: "", + describe: "base path to serve your local server on", + }, + port: { + number: true, + default: 8080, + describe: "port to serve Quartz on", + }, + wsPort: { + number: true, + default: 3001, + describe: "port to use for WebSocket-based hot-reload notifications", + }, + remoteDevHost: { + string: true, + default: "", + describe: "A URL override for the websocket connection if you are not developing on localhost", + }, + bundleInfo: { + boolean: true, + default: false, + describe: "show detailed bundle information", + }, +} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/constants.js b/Local/storage/thlab-notes/worker/quartz/cli/constants.js new file mode 100644 index 0000000..83c702a --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/constants.js @@ -0,0 +1,16 @@ +import path from "path" +import { readFileSync } from "fs" + +/** + * All constants relating to helpers or handlers + */ +export const ORIGIN_NAME = "origin" +export const UPSTREAM_NAME = "upstream" +export const QUARTZ_SOURCE_BRANCH = "v5" +export const QUARTZ_SOURCE_REPO = "https://github.com/jackyzha0/quartz.git" +export const cwd = process.cwd() +export const cacheDir = path.join(cwd, ".quartz-cache") +export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs" +export const fp = "./quartz/build.ts" +export const { version } = JSON.parse(readFileSync("./package.json").toString()) +export const contentCacheFolder = path.join(cacheDir, "content-cache") diff --git a/Local/storage/thlab-notes/worker/quartz/cli/handlers.js b/Local/storage/thlab-notes/worker/quartz/cli/handlers.js new file mode 100644 index 0000000..6d4119c --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/handlers.js @@ -0,0 +1,801 @@ +import { promises } from "fs" +import path from "path" +import esbuild from "esbuild" +import { styleText } from "util" +import { sassPlugin } from "esbuild-sass-plugin" +import fs from "fs" +import { intro, outro, select, text } from "@clack/prompts" +import { rm } from "fs/promises" +import chokidar from "chokidar" +import prettyBytes from "pretty-bytes" +import { execSync, spawnSync } from "child_process" +import http from "http" +import serveHandler from "serve-handler" +import { WebSocketServer } from "ws" +import { randomUUID } from "crypto" +import { Mutex } from "async-mutex" +import { CreateArgv } from "./args.js" +import { globby } from "globby" +import { + exitIfCancel, + escapePath, + gitPull, + popContentFolder, + stashContentFolder, + symlinkOrCopy, +} from "./helpers.js" +import { + handlePluginRestore, + handlePluginCheck, + handlePluginResolve, +} from "./plugin-git-handlers.js" +import { + configExists, + createConfigFromDefault, + createConfigFromTemplate, + readPluginsJson, + writePluginsJson, + extractPluginName, + updateGlobalConfig, + LOCKFILE_PATH, +} from "./plugin-data.js" +import { + UPSTREAM_NAME, + QUARTZ_SOURCE_BRANCH, + QUARTZ_SOURCE_REPO, + ORIGIN_NAME, + version, + fp, + cacheFile, + cwd, +} from "./constants.js" + +/** + * Resolve content directory path + * @param contentPath path to resolve + */ +function resolveContentPath(contentPath) { + if (path.isAbsolute(contentPath)) return path.relative(cwd, contentPath) + return path.join(cwd, contentPath) +} + +/** + * Handles `npx quartz create` + * @param {*} argv arguments for `create` + */ +export async function handleCreate(argv) { + console.log() + intro(styleText(["bgGreen", "black"], ` Quartz v${version} `)) + const contentFolder = resolveContentPath(argv.directory) + let setupStrategy = argv.strategy?.toLowerCase() + let linkResolutionStrategy = argv.links?.toLowerCase() + const sourceDirectory = argv.source + let template = argv.template?.toLowerCase() + let baseUrl = argv.baseUrl + + // If all cmd arguments were provided, check if they're valid + if (setupStrategy && linkResolutionStrategy) { + // If setup isn't, "new", source argument is required + if (setupStrategy !== "new") { + // Error handling + if (!sourceDirectory) { + outro( + styleText( + "red", + `Setup strategies (arg '${styleText( + "yellow", + `-${CreateArgv.strategy.alias[0]}`, + )}') other than '${styleText( + "yellow", + "new", + )}' require content folder argument ('${styleText( + "yellow", + `-${CreateArgv.source.alias[0]}`, + )}') to be set`, + ), + ) + process.exit(1) + } else { + if (!fs.existsSync(sourceDirectory)) { + outro( + styleText( + "red", + `Input directory to copy/symlink 'content' from not found ('${styleText( + "yellow", + sourceDirectory, + )}', invalid argument "${styleText("yellow", `-${CreateArgv.source.alias[0]}`)})`, + ), + ) + process.exit(1) + } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { + outro( + styleText( + "red", + `Source directory to copy/symlink 'content' from is not a directory (found file at '${styleText( + "yellow", + sourceDirectory, + )}', invalid argument ${styleText("yellow", `-${CreateArgv.source.alias[0]}`)}")`, + ), + ) + process.exit(1) + } + } + } + } + + // Template selection + if (!template) { + template = exitIfCancel( + await select({ + message: "Choose a template for your Quartz configuration", + options: [ + { value: "default", label: "Default", hint: "clean Quartz setup with sensible defaults" }, + { + value: "obsidian", + label: "Obsidian", + hint: "optimized for Obsidian vaults with full OFM support", + }, + { + value: "ttrpg", + label: "TTRPG", + hint: "Obsidian + map plugin + ITS Theme for D&D/TTRPG wikis", + }, + { + value: "blog", + label: "Blog", + hint: "recent notes and comments enabled for blogging", + }, + ], + }), + ) + } + // Use cli process if cmd args werent provided + if (!setupStrategy) { + setupStrategy = exitIfCancel( + await select({ + message: `Choose how to initialize the content in \`${contentFolder}\``, + options: [ + { value: "new", label: "Empty Quartz" }, + { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" }, + { + value: "symlink", + label: "Symlink an existing folder", + hint: "don't select this unless you know what you are doing!", + }, + ], + }), + ) + } + + async function rmContentFolder() { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + await fs.promises.unlink(contentFolder) + } else { + await rm(contentFolder, { recursive: true, force: true }) + } + } + + const gitkeepPath = path.join(contentFolder, ".gitkeep") + if (fs.existsSync(gitkeepPath)) { + await fs.promises.unlink(gitkeepPath) + } + if (setupStrategy === "copy" || setupStrategy === "symlink") { + let originalFolder = sourceDirectory + + // If input directory was not passed, use cli + if (!sourceDirectory) { + originalFolder = escapePath( + exitIfCancel( + await text({ + message: "Enter the full path to existing content folder", + placeholder: + "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", + validate(fp) { + const fullPath = escapePath(fp) + if (!fs.existsSync(fullPath)) { + return "The given path doesn't exist" + } else if (!fs.lstatSync(fullPath).isDirectory()) { + return "The given path is not a folder" + } + }, + }), + ), + ) + } + + await rmContentFolder() + if (setupStrategy === "copy") { + await fs.promises.cp(originalFolder, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } else if (setupStrategy === "symlink") { + await symlinkOrCopy(originalFolder, contentFolder) + } + } else if (setupStrategy === "new") { + await fs.promises.writeFile( + path.join(contentFolder, "index.md"), + `--- +title: Welcome to Quartz +--- + +This is a blank Quartz installation. +See the [documentation](https://quartz.jzhao.xyz) for how to get started. +`, + ) + } + + // Obsidian and TTRPG templates auto-set link resolution to "shortest" + const skipLinkPrompt = template === "obsidian" || template === "ttrpg" + if (skipLinkPrompt) { + linkResolutionStrategy = "shortest" + } + + // Use cli process if cmd args werent provided + if (!linkResolutionStrategy) { + // get a preferred link resolution strategy + linkResolutionStrategy = exitIfCancel( + await select({ + message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.yaml\`.`, + options: [ + { + value: "shortest", + label: "Treat links as shortest path", + hint: "(default)", + }, + { + value: "absolute", + label: "Treat links as absolute path", + }, + { + value: "relative", + label: "Treat links as relative paths", + }, + ], + }), + ) + } + + // Base URL prompt + if (!baseUrl) { + baseUrl = exitIfCancel( + await text({ + message: "Enter the base URL for your Quartz site (e.g. mysite.github.io/quartz)", + placeholder: "mysite.github.io", + validate(value) { + if (!value || value.trim().length === 0) { + return "Base URL cannot be empty" + } + }, + }), + ) + } + + // Strip protocol prefix if user included it + baseUrl = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "") + + if (template && template !== "default") { + createConfigFromTemplate(template) + console.log(styleText("green", `Created quartz.config.yaml from '${template}' template`)) + } else { + createConfigFromTemplate("default") + console.log(styleText("green", "Created quartz.config.yaml from defaults")) + } + + // Update markdownLinkResolution in the crawl-links plugin options via YAML config + const json = readPluginsJson() + if (json?.plugins) { + const crawlLinksIndex = json.plugins.findIndex( + (p) => extractPluginName(p.source) === "crawl-links", + ) + if (crawlLinksIndex !== -1) { + json.plugins[crawlLinksIndex].options = { + ...json.plugins[crawlLinksIndex].options, + markdownLinkResolution: linkResolutionStrategy, + } + writePluginsJson(json) + } + } + + // Update baseUrl in configuration + updateGlobalConfig({ baseUrl }) + + // install plugins referenced in the template config + await handlePluginResolve() + + // setup remote + execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`, { + stdio: "ignore", + }) + + outro(`You're all set! Not sure what to do next? Try: + • Customizing Quartz a bit more by editing \`quartz.config.yaml\` + • Running \`npx quartz build --serve\` to preview your Quartz locally + • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting) +`) +} + +/** + * Handles `npx quartz build` + * @param {*} argv arguments for `build` + */ +export async function handleBuild(argv) { + if (argv.concurrency !== undefined && argv.concurrency < 1) { + console.error("Concurrency must be at least 1") + process.exit(1) + } + + if (argv.serve) { + argv.watch = true + } + + console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`) + const ctx = await esbuild.context({ + entryPoints: [fp], + outfile: cacheFile, + bundle: true, + keepNames: true, + minifyWhitespace: true, + minifySyntax: true, + platform: "node", + format: "esm", + jsx: "automatic", + jsxImportSource: "preact", + packages: "external", + metafile: true, + sourcemap: true, + sourcesContent: false, + logOverride: { + "direct-eval": "silent", + "equals-negative-zero": "silent", + "duplicate-object-key": "silent", + }, + plugins: [ + sassPlugin({ + type: "css-text", + cssImports: true, + }), + sassPlugin({ + filter: /\.inline\.scss$/, + type: "css", + cssImports: true, + }), + { + name: "inline-script-loader", + setup(build) { + build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => { + let text = await promises.readFile(args.path, "utf8") + + // remove default exports that we manually inserted + text = text.replace("export default", "") + text = text.replace("export", "") + + const sourcefile = path.relative(path.resolve("."), args.path) + const resolveDir = path.dirname(sourcefile) + const transpiled = await esbuild.build({ + stdin: { + contents: text, + loader: "ts", + resolveDir, + sourcefile, + }, + write: false, + bundle: true, + minify: true, + platform: "browser", + format: "esm", + }) + const rawMod = transpiled.outputFiles[0].text + return { + contents: rawMod, + loader: "text", + } + }) + }, + }, + ], + }) + + const buildMutex = new Mutex() + let lastBuildMs = 0 + let cleanupBuild = null + const build = async (clientRefresh) => { + const buildStart = new Date().getTime() + lastBuildMs = buildStart + const release = await buildMutex.acquire() + if (lastBuildMs > buildStart) { + release() + return + } + + if (cleanupBuild) { + console.log(styleText("yellow", "Detected a source code change, doing a hard rebuild...")) + await cleanupBuild() + } + + const result = await ctx.rebuild().catch((err) => { + console.error( + `${styleText("red", "Failed to build Quartz.")} Check for syntax errors in your configuration or plugins.`, + ) + console.log(`Reason: ${styleText("gray", err.message ?? String(err))}`) + process.exit(1) + }) + release() + + if (argv.bundleInfo) { + const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs" + const meta = result.metafile.outputs[outputFileName] + console.log( + `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes( + meta.bytes, + )})`, + ) + console.log(await esbuild.analyzeMetafile(result.metafile, { color: true })) + } + + // bypass module cache + // https://github.com/nodejs/modules/issues/307 + const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`) + // ^ this import is relative, so base "cacheFile" path can't be used + + cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh) + clientRefresh() + } + + let clientRefresh = () => {} + if (argv.serve) { + const connections = [] + clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) + + if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) { + argv.baseDir = "/" + argv.baseDir + } + + await build(clientRefresh) + const server = http.createServer(async (req, res) => { + if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) { + console.log( + styleText( + "red", + `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`, + ), + ) + res.writeHead(404) + res.end() + return + } + + // strip baseDir prefix + req.url = req.url?.slice(argv.baseDir.length) + + const serve = async () => { + const release = await buildMutex.acquire() + await serveHandler(req, res, { + public: argv.output, + directoryListing: false, + headers: [ + { + source: "**/*.*", + headers: [{ key: "Content-Disposition", value: "inline" }], + }, + { + source: "**/*.webp", + headers: [{ key: "Content-Type", value: "image/webp" }], + }, + // fixes bug where avif images are displayed as text instead of images (future proof) + { + source: "**/*.avif", + headers: [{ key: "Content-Type", value: "image/avif" }], + }, + ], + }) + const status = res.statusCode + const statusString = + status >= 200 && status < 300 + ? styleText("green", `[${status}]`) + : styleText("red", `[${status}]`) + console.log(statusString + styleText("gray", ` ${argv.baseDir}${req.url}`)) + release() + } + + const redirect = (newFp) => { + newFp = argv.baseDir + newFp + res.writeHead(302, { + Location: newFp, + }) + console.log( + styleText("yellow", "[302]") + + styleText("gray", ` ${argv.baseDir}${req.url} -> ${newFp}`), + ) + res.end() + } + + let fp = req.url?.split("?")[0] ?? "/" + + // handle redirects + if (fp.endsWith("/")) { + // /trailing/ + // does /trailing/index.html exist? if so, serve it + const indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + req.url = fp + return serve() + } + + // does /trailing.html exist? if so, redirect to /trailing + let base = fp.slice(0, -1) + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + return redirect(fp.slice(0, -1)) + } + } else { + // /regular + // does /regular.html exist? if so, serve it + let base = fp + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + req.url = fp + return serve() + } + + // does /regular/index.html exist? if so, redirect to /regular/ + let indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + return redirect(fp + "/") + } + } + + return serve() + }) + + server.on("error", (err) => { + if (err.code === "EADDRINUSE") { + console.error( + `Port ${argv.port} is already in use. Try a different port with --port `, + ) + process.exit(1) + } + throw err + }) + server.listen(argv.port) + const wss = new WebSocketServer({ port: argv.wsPort }) + wss.on("error", (err) => { + if (err.code === "EADDRINUSE") { + console.error( + `WebSocket port ${argv.wsPort} is already in use. Try a different port with --wsPort `, + ) + process.exit(1) + } + throw err + }) + wss.on("connection", (ws) => connections.push(ws)) + console.log( + styleText( + "cyan", + `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`, + ), + ) + } else { + await build(clientRefresh) + ctx.dispose() + } + + if (argv.watch) { + const paths = await globby([ + "**/*.ts", + "quartz/cli/*.js", + "quartz/static/**/*", + "**/*.tsx", + "**/*.scss", + "package.json", + "quartz.config.yaml", + "quartz.config.default.yaml", + ]) + chokidar + .watch(paths, { ignoreInitial: true }) + .on("add", () => build(clientRefresh)) + .on("change", () => build(clientRefresh)) + .on("unlink", () => build(clientRefresh)) + + console.log(styleText("gray", "hint: exit with ctrl+c")) + } +} + +/** + * Handles `npx quartz upgrade` + * Upgrades the Quartz framework itself by pulling latest changes from upstream. + * @param {*} argv arguments for `upgrade` + */ +export async function handleUpgrade(argv) { + const contentFolder = resolveContentPath(argv.directory) + console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`) + console.log("Backing up your content") + execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`) + await stashContentFolder(contentFolder) + + const lockfileBackup = LOCKFILE_PATH + ".bak" + const hasLockfile = fs.existsSync(LOCKFILE_PATH) + if (hasLockfile) { + fs.copyFileSync(LOCKFILE_PATH, lockfileBackup) + } + + console.log( + "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.", + ) + + let pullOk = false + try { + gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH) + pullOk = true + } catch { + if (hasLockfile) { + try { + fs.copyFileSync(lockfileBackup, LOCKFILE_PATH) + execSync(`git add ${LOCKFILE_PATH}`) + const remaining = execSync("git diff --name-only --diff-filter=U", { + encoding: "utf-8", + }).trim() + if (remaining.length === 0) { + execSync("git commit --no-edit") + pullOk = true + console.log(styleText("cyan", "Resolved quartz.lock.json merge conflict automatically.")) + } + } catch { + // Could not auto-resolve, fall through to manual resolution + } + } + + if (!pullOk) { + console.log( + styleText("red", "An error occurred while pulling updates.") + + "\nCheck your network connection and git credentials. If you see merge conflicts, resolve them manually and run `npx quartz sync --no-pull`.", + ) + await popContentFolder(contentFolder) + if (fs.existsSync(lockfileBackup)) fs.unlinkSync(lockfileBackup) + return + } + } + + if (hasLockfile && fs.existsSync(lockfileBackup)) { + fs.copyFileSync(lockfileBackup, LOCKFILE_PATH) + fs.unlinkSync(lockfileBackup) + } + + await popContentFolder(contentFolder) + + // Read the new version after pulling + const newPkg = JSON.parse(fs.readFileSync("./package.json").toString()) + const newVersion = newPkg.version + if (newVersion !== version) { + console.log(styleText("cyan", `Upgraded Quartz: v${version} → v${newVersion}`)) + } else { + console.log(styleText("gray", `Quartz is already up to date (v${version})`)) + } + + console.log("Ensuring dependencies are up to date") + + /* + On Windows, if the command `npm` is really `npm.cmd', this call fails + as it will be unable to find `npm`. This is often the case on systems + where `npm` is installed via a package manager. + + This means `npx quartz upgrade` will not actually update dependencies + on Windows, without a manual `npm i` from the caller. + + However, by spawning a shell, we are able to call `npm.cmd`. + See: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows + */ + + const opts = { stdio: "inherit" } + if (process.platform === "win32") { + opts.shell = true + } + + const res = spawnSync("npm", ["i"], opts) + if (res.status === 0) { + console.log(styleText("green", "Dependencies updated!")) + } else { + console.log( + styleText("red", "An error occurred while installing dependencies.") + + "\nTry running `npm install` manually to see detailed errors.", + ) + } + + console.log("Restoring plugins from lockfile...") + await handlePluginRestore() + + console.log("Checking plugin compatibility...") + await handlePluginCheck() + + console.log(styleText("green", "Done!")) +} + +/** + * Handles `npx quartz restore` + * @param {*} argv arguments for `restore` + */ +export async function handleRestore(argv) { + const contentFolder = resolveContentPath(argv.directory) + await popContentFolder(contentFolder) +} + +/** + * Handles `npx quartz sync` + * @param {*} argv arguments for `sync` + */ +export async function handleSync(argv) { + const contentFolder = resolveContentPath(argv.directory) + console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)}\n`) + console.log("Backing up your content") + + if (argv.commit) { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + const linkTarg = await fs.promises.readlink(contentFolder) + console.log(styleText("yellow", "Detected symlink, trying to dereference before committing")) + + // stash symlink file + await stashContentFolder(contentFolder) + + // follow symlink and copy content + await fs.promises.cp(linkTarg, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } + + const currentTimestamp = new Date().toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}` + spawnSync("git", ["add", "."], { stdio: "inherit" }) + spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" }) + + if (contentStat.isSymbolicLink()) { + // put symlink back + await popContentFolder(contentFolder) + } + } + + await stashContentFolder(contentFolder) + + if (argv.pull) { + console.log( + "Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.", + ) + try { + gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) + } catch { + console.log( + styleText("red", "An error occurred while pulling updates from your repository.") + + "\nCheck your network connection and git credentials.", + ) + await popContentFolder(contentFolder) + return + } + } + + await popContentFolder(contentFolder) + if (argv.push) { + console.log("Pushing your changes") + const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim() + const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], { + stdio: "inherit", + }) + if (res.status !== 0) { + console.log( + styleText("red", `An error occurred while pushing to remote ${ORIGIN_NAME}.`) + + "\nCheck that you have push access to the remote repository.", + ) + return + } + } + + console.log(styleText("green", "Done!")) +} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/helpers.js b/Local/storage/thlab-notes/worker/quartz/cli/helpers.js new file mode 100644 index 0000000..75334c7 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/helpers.js @@ -0,0 +1,109 @@ +import { isCancel, outro } from "@clack/prompts" +import { styleText } from "util" +import { contentCacheFolder } from "./constants.js" +import { spawnSync } from "child_process" +import fs from "fs" +import path from "path" + +export function escapePath(fp) { + return fp + .replace(/\\ /g, " ") // unescape spaces + .replace(/^"(.*)"$/, "$1") + .replace(/^'(.*)'$/, "$1") + .trim() +} + +export function exitIfCancel(val) { + if (isCancel(val)) { + outro(styleText("red", "Exiting")) + process.exit(0) + } else { + return val + } +} + +export async function stashContentFolder(contentFolder) { + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) + await fs.promises.cp(contentFolder, contentCacheFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentFolder, { force: true, recursive: true }) +} + +export function gitPull(origin, branch) { + const flags = ["--no-rebase", "--autostash", "--no-edit", "--allow-unrelated-histories"] + const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) + if (out.stderr) { + throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`)) + } else if (out.status !== 0) { + throw new Error(styleText("red", "Error while pulling updates")) + } +} + +export async function popContentFolder(contentFolder) { + await fs.promises.rm(contentFolder, { force: true, recursive: true }) + await fs.promises.cp(contentCacheFolder, contentFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) +} + +/** + * Create a directory symlink with Windows fallback. + * + * On Windows, creating symlinks requires Developer Mode or admin privileges. + * When that fails (EPERM), we try a junction first (no elevation needed), + * then fall back to a recursive copy as a last resort. + * + * @param {string} target Symlink target (may be relative) + * @param {string} linkPath Path where the link is created + */ +export function symlinkOrCopySync(target, linkPath) { + try { + fs.symlinkSync(target, linkPath, "dir") + } catch (err) { + if (err.code === "EEXIST") return + if (err.code === "EPERM" && process.platform === "win32") { + try { + fs.symlinkSync(target, linkPath, "junction") + return + } catch { + const resolvedTarget = path.resolve(path.dirname(linkPath), target) + fs.cpSync(resolvedTarget, linkPath, { recursive: true }) + return + } + } + throw err + } +} + +/** + * Async version of {@link symlinkOrCopySync}. + * + * @param {string} target Symlink target (may be relative) + * @param {string} linkPath Path where the link is created + */ +export async function symlinkOrCopy(target, linkPath) { + try { + await fs.promises.symlink(target, linkPath, "dir") + } catch (err) { + if (err.code === "EEXIST") return + if (err.code === "EPERM" && process.platform === "win32") { + try { + await fs.promises.symlink(target, linkPath, "junction") + return + } catch { + const resolvedTarget = path.resolve(path.dirname(linkPath), target) + await fs.promises.cp(resolvedTarget, linkPath, { recursive: true }) + return + } + } + throw err + } +} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/helpers.test.js b/Local/storage/thlab-notes/worker/quartz/cli/helpers.test.js new file mode 100644 index 0000000..b3890bd --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/helpers.test.js @@ -0,0 +1,237 @@ +import test, { describe, beforeEach, afterEach, mock } from "node:test" +import assert from "node:assert" +import fs from "fs" +import path from "path" +import os from "os" +import { symlinkOrCopySync, symlinkOrCopy } from "./helpers.js" + +function makeTmpDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), "quartz-symlink-test-")) +} + +function makeTarget(tmpDir) { + const target = path.join(tmpDir, "target") + fs.mkdirSync(target) + fs.writeFileSync(path.join(target, "marker.txt"), "hello") + return target +} + +describe("symlinkOrCopySync", () => { + let tmpDir + + beforeEach(() => { + tmpDir = makeTmpDir() + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + test("creates a symlink on success", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + symlinkOrCopySync(target, linkPath) + + const stat = fs.lstatSync(linkPath) + assert.ok(stat.isSymbolicLink()) + assert.ok(fs.existsSync(path.join(linkPath, "marker.txt"))) + }) + + test("silently succeeds when link already exists (EEXIST)", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + symlinkOrCopySync(target, linkPath) + assert.doesNotThrow(() => symlinkOrCopySync(target, linkPath)) + }) + + test("re-throws non-EPERM errors", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "nonexistent-parent", "link") + + assert.throws( + () => symlinkOrCopySync(target, linkPath), + (err) => { + return err.code === "ENOENT" + }, + ) + }) + + test("falls back to junction on Windows EPERM", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + let callCount = 0 + const originalSymlinkSync = fs.symlinkSync + mock.method(fs, "symlinkSync", (t, lp, type) => { + callCount++ + if (callCount === 1 && type === "dir") { + const err = new Error("EPERM: operation not permitted, symlink") + err.code = "EPERM" + err.errno = -4048 + err.syscall = "symlink" + throw err + } + return originalSymlinkSync(t, lp, type) + }) + + try { + symlinkOrCopySync(target, linkPath) + assert.ok(fs.existsSync(path.join(linkPath, "marker.txt"))) + assert.strictEqual(callCount, 2) + } finally { + fs.symlinkSync.mock.restore() + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + } + }) + + test("falls back to copy when both symlink and junction fail on Windows", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + const originalSymlinkSync = fs.symlinkSync + mock.method(fs, "symlinkSync", (_t, _lp, _type) => { + const err = new Error("EPERM: operation not permitted, symlink") + err.code = "EPERM" + err.errno = -4048 + err.syscall = "symlink" + throw err + }) + + try { + symlinkOrCopySync(target, linkPath) + + const stat = fs.lstatSync(linkPath) + assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink") + assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello") + } finally { + fs.symlinkSync.mock.restore() + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + } + }) + + test("does not fall back on EPERM when not on Windows", () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + + const originalSymlinkSync = fs.symlinkSync + mock.method(fs, "symlinkSync", (_t, _lp, _type) => { + const err = new Error("EPERM: operation not permitted, symlink") + err.code = "EPERM" + throw err + }) + + try { + assert.throws( + () => symlinkOrCopySync(target, linkPath), + (err) => err.code === "EPERM", + ) + } finally { + fs.symlinkSync.mock.restore() + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + } + }) +}) + +describe("symlinkOrCopy", () => { + let tmpDir + + beforeEach(() => { + tmpDir = makeTmpDir() + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + test("creates a symlink on success", async () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + await symlinkOrCopy(target, linkPath) + + const stat = fs.lstatSync(linkPath) + assert.ok(stat.isSymbolicLink()) + assert.ok(fs.existsSync(path.join(linkPath, "marker.txt"))) + }) + + test("silently succeeds when link already exists (EEXIST)", async () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + await symlinkOrCopy(target, linkPath) + await assert.doesNotReject(() => symlinkOrCopy(target, linkPath)) + }) + + test("falls back to copy when both symlink and junction fail on Windows", async () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + const originalSymlink = fs.promises.symlink + mock.method(fs.promises, "symlink", async (_t, _lp, _type) => { + const err = new Error("EPERM: operation not permitted, symlink") + err.code = "EPERM" + err.errno = -4048 + err.syscall = "symlink" + throw err + }) + + try { + await symlinkOrCopy(target, linkPath) + + const stat = fs.lstatSync(linkPath) + assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink") + assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello") + } finally { + fs.promises.symlink.mock.restore() + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + } + }) + + test("does not fall back on EPERM when not on Windows", async () => { + const target = makeTarget(tmpDir) + const linkPath = path.join(tmpDir, "link") + + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + + mock.method(fs.promises, "symlink", async (_t, _lp, _type) => { + const err = new Error("EPERM: operation not permitted, symlink") + err.code = "EPERM" + throw err + }) + + try { + await assert.rejects( + () => symlinkOrCopy(target, linkPath), + (err) => err.code === "EPERM", + ) + } finally { + fs.promises.symlink.mock.restore() + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + } + }) +}) diff --git a/Local/storage/thlab-notes/worker/quartz/cli/plugin-data.js b/Local/storage/thlab-notes/worker/quartz/cli/plugin-data.js new file mode 100644 index 0000000..c8cd6b4 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/plugin-data.js @@ -0,0 +1,367 @@ +import fs from "fs" +import path from "path" +import { execSync } from "child_process" +import YAML from "yaml" + +const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json") +const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins") +const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml") +const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml") +const TEMPLATES_DIR = path.join(process.cwd(), "quartz", "cli", "templates") + +const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json") +const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json") + +function resolveConfigPath() { + if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH + if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH + if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH + if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH + return CONFIG_YAML_PATH +} + +function resolveDefaultConfigPath() { + if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH + if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH + return DEFAULT_CONFIG_YAML_PATH +} + +function readFileAsData(filePath) { + if (!fs.existsSync(filePath)) return null + try { + const raw = fs.readFileSync(filePath, "utf-8") + if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) { + return YAML.parse(raw) + } + return JSON.parse(raw) + } catch { + return null + } +} + +function writeDataToFile(filePath, data) { + if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) { + const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n" + fs.writeFileSync(filePath, header + YAML.stringify(data, { lineWidth: 120 })) + } else { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n") + } +} + +export function readPluginsJson() { + const configPath = resolveConfigPath() + return readFileAsData(configPath) +} + +export function writePluginsJson(data) { + const { $schema, ...rest } = data + writeDataToFile(CONFIG_YAML_PATH, rest) +} + +function readDefaultPluginsJson() { + const defaultPath = resolveDefaultConfigPath() + return readFileAsData(defaultPath) +} + +export function readLockfile() { + if (!fs.existsSync(LOCKFILE_PATH)) return null + try { + return JSON.parse(fs.readFileSync(LOCKFILE_PATH, "utf-8")) + } catch { + return null + } +} + +export function writeLockfile(lockfile) { + if (lockfile.plugins) { + const sorted = {} + for (const key of Object.keys(lockfile.plugins).sort()) { + sorted[key] = lockfile.plugins[key] + } + lockfile = { ...lockfile, plugins: sorted } + } + fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2) + "\n") +} + +/** + * Normalizes a source value to a URL string. + * Source can be a plain string (e.g. "github:owner/repo") or an object + * with { name?, repo, subdir? } for installing from a subdirectory of a repo. + */ +export function getSourceUrl(source) { + if (typeof source === "string") return source + if (typeof source === "object" && source !== null && typeof source.repo === "string") { + return source.repo + } + throw new Error(`Invalid plugin source: ${JSON.stringify(source)}`) +} + +/** + * Returns the subdir from an object source, or undefined for string sources. + */ +function getSourceSubdir(source) { + if (typeof source === "object" && source !== null && typeof source.subdir === "string") { + return source.subdir + } + return undefined +} + +/** + * Returns a display-friendly string for a source value. + */ +export function formatSource(source) { + if (typeof source === "string") return source + if (typeof source === "object" && source !== null) { + const parts = [source.repo] + if (source.subdir) parts.push(`(subdir: ${source.subdir})`) + return parts.join(" ") + } + return String(source) +} + +export function isLocalSource(source) { + const url = getSourceUrl(source) + if (url.startsWith("./") || url.startsWith("../") || url.startsWith("/")) { + return true + } + // Windows absolute paths (e.g. C:\ or D:/) + if (/^[A-Za-z]:[\\/]/.test(url)) { + return true + } + return false +} +export function extractPluginName(source) { + if (typeof source === "object" && source !== null && typeof source.name === "string") { + return source.name + } + const url = getSourceUrl(source) + if (isLocalSource(url)) { + return path.basename(url.replace(/[\/]+$/, "")) + } + if (url.startsWith("github:")) { + const withoutPrefix = url.replace("github:", "") + const [repoPath] = withoutPrefix.split("#") + const parts = repoPath.split("/") + return parts[parts.length - 1] + } + if (url.startsWith("git+") || url.startsWith("https://")) { + const cleaned = url.replace("git+", "") + const match = cleaned.match(/\/([^/]+?)(?:\.git)?(?:#|$)/) + return match?.[1] ?? url + } + return url +} + +export function readManifestFromPackageJson(pluginDir) { + const pkgPath = path.join(pluginDir, "package.json") + if (!fs.existsSync(pkgPath)) return null + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) + return pkg.quartz ?? null + } catch { + return null + } +} + +export function parseGitSource(source) { + const url = getSourceUrl(source) + const subdir = getSourceSubdir(source) + if (isLocalSource(url)) { + const resolved = path.resolve(url) + const name = typeof source === "object" && source.name ? source.name : path.basename(resolved) + return { name, url: resolved, ref: undefined, local: true, subdir } + } + if (url.startsWith("github:")) { + const [repoPath, ref] = url.replace("github:", "").split("#") + const [owner, repo] = repoPath.split("/") + const name = typeof source === "object" && source.name ? source.name : repo + return { name, url: `https://github.com/${owner}/${repo}.git`, ref, subdir } + } + if (url.startsWith("git+")) { + const raw = url.replace("git+", "") + const [parsed, ref] = raw.split("#") + const name = + typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git") + return { name, url: parsed, ref, subdir } + } + if (url.startsWith("https://")) { + const [parsed, ref] = url.split("#") + const name = + typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git") + return { name, url: parsed, ref, subdir } + } + throw new Error(`Cannot parse plugin source: ${formatSource(source)}`) +} + +export function getGitCommit(pluginDir) { + try { + return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim() + } catch { + return "unknown" + } +} + +export function updateGlobalConfig(updates) { + const json = readPluginsJson() + if (!json) return false + json.configuration = { ...json.configuration, ...updates } + writePluginsJson(json) + return true +} + +export function configExists() { + return fs.existsSync(CONFIG_YAML_PATH) || fs.existsSync(LEGACY_PLUGINS_JSON_PATH) +} + +export function createConfigFromDefault() { + const defaultData = readDefaultPluginsJson() + if (!defaultData) { + // No default available — create minimal config + const minimal = { + configuration: { + pageTitle: "Quartz", + enableSPA: true, + enablePopovers: true, + analytics: { provider: "plausible" }, + locale: "en-US", + baseUrl: "quartz.jzhao.xyz", + ignorePatterns: ["private", "templates", ".obsidian"], + theme: { + cdnCaching: true, + typography: { + header: "Schibsted Grotesk", + body: "Source Sans Pro", + code: "IBM Plex Mono", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "#161618", + lightgray: "#393639", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + }, + }, + }, + plugins: [], + layout: { groups: {}, byPageType: {} }, + } + writePluginsJson(minimal) + return minimal + } + + const { $schema, ...rest } = defaultData + writePluginsJson(rest) + return rest +} + +const VALID_TEMPLATES = ["default", "obsidian", "ttrpg", "blog"] + +export function createConfigFromTemplate(templateName) { + if (!VALID_TEMPLATES.includes(templateName)) { + throw new Error( + `Unknown template: ${templateName}. Valid templates: ${VALID_TEMPLATES.join(", ")}`, + ) + } + + const templatePath = path.join(TEMPLATES_DIR, `${templateName}.yaml`) + const templateData = readFileAsData(templatePath) + if (!templateData) { + // Template file missing — fall back to default config creation + return createConfigFromDefault() + } + + const { $schema, ...rest } = templateData + writePluginsJson(rest) + return rest +} + +/** + * Resolves a user-facing plugin name (which may be an overridden name from config) + * to the corresponding lockfile key (the original name at install time). + * + * This bridges the naming identity split between config YAML (which supports + * source.name overrides) and the lockfile/disk (which are keyed by the original name). + * + * @param {string} name - The name the user provided (may be overridden or original) + * @param {object|null} lockfile - The parsed lockfile + * @param {object|null} pluginsJson - The parsed config YAML + * @returns {string} The lockfile key that corresponds to this plugin + */ +export function resolveLockfileName(name, lockfile, pluginsJson) { + // Direct match — no resolution needed + if (lockfile?.plugins?.[name]) return name + + // Check if any config entry with this overridden name maps to a different lockfile key + if (pluginsJson?.plugins) { + const configEntry = pluginsJson.plugins.find( + (e) => extractPluginName(e.source) === name || formatSource(e.source) === name, + ) + if (configEntry) { + const url = getSourceUrl(configEntry.source) + for (const [key, lock] of Object.entries(lockfile?.plugins ?? {})) { + if ( + lock.source === url || + lock.source === formatSource(configEntry.source) || + lock.resolved === url + ) { + return key + } + } + } + } + + return name +} + +/** + * Builds a map from lockfile keys to their overridden display names from config. + * Returns entries only where the overridden name differs from the lockfile key. + * + * @param {object|null} lockfile - The parsed lockfile + * @param {object|null} pluginsJson - The parsed config YAML + * @returns {Map} Map of lockfileKey → overriddenName + */ +export function getNameOverrides(lockfile, pluginsJson) { + const overrides = new Map() + if (!lockfile?.plugins || !pluginsJson?.plugins) return overrides + + for (const entry of pluginsJson.plugins) { + const configName = extractPluginName(entry.source) + const url = getSourceUrl(entry.source) + + for (const [lockKey, lock] of Object.entries(lockfile.plugins)) { + if (lockKey === configName) break // no override, names match + if ( + lock.source === url || + lock.source === formatSource(entry.source) || + lock.resolved === url + ) { + overrides.set(lockKey, configName) + break + } + } + } + + return overrides +} + +export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH +export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH +export { LOCKFILE_PATH, PLUGINS_DIR } diff --git a/Local/storage/thlab-notes/worker/quartz/cli/plugin-git-handlers.js b/Local/storage/thlab-notes/worker/quartz/cli/plugin-git-handlers.js new file mode 100644 index 0000000..fb475cd --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/plugin-git-handlers.js @@ -0,0 +1,1759 @@ +import fs from "fs" +import path from "path" +import os from "os" +import { exec as execCb } from "child_process" +import { styleText, promisify } from "util" +import { + readPluginsJson, + writePluginsJson, + readLockfile, + writeLockfile, + extractPluginName, + readManifestFromPackageJson, + parseGitSource, + getGitCommit, + PLUGINS_DIR, + LOCKFILE_PATH, + isLocalSource, + getSourceUrl, + formatSource, + resolveLockfileName, + getNameOverrides, +} from "./plugin-data.js" +import { symlinkOrCopySync } from "./helpers.js" + +const INTERNAL_EXPORTS = new Set(["manifest", "default"]) + +const execAsync = promisify(execCb) + +async function cloneWithSubdirAsync({ url, ref, subdir, pluginDir }) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "quartz-plugin-")) + try { + if (ref) { + await execAsync(`git clone --depth 1 --branch ${ref} "${url}" "${tmpDir}"`) + } else { + await execAsync(`git clone --depth 1 "${url}" "${tmpDir}"`) + } + const subdirPath = path.join(tmpDir, subdir) + if (!fs.existsSync(subdirPath)) { + throw new Error(`Subdirectory "${subdir}" not found in cloned repository`) + } + fs.cpSync(subdirPath, pluginDir, { recursive: true }) + const { stdout } = await execAsync("git rev-parse HEAD", { cwd: tmpDir }) + return stdout.trim() + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } +} + +async function buildPluginAsync(pluginDir, name) { + if (hasPrebuiltDist(pluginDir)) { + console.log(styleText("green", ` ✓ ${name}: using pre-built dist/`)) + linkPeerPlugins(pluginDir) + return true + } + + try { + const skipBuild = !needsBuild(pluginDir) + console.log(styleText("cyan", ` → ${name}: installing dependencies...`)) + await execAsync("npm install --ignore-scripts", { cwd: pluginDir }) + if (!skipBuild) { + console.log(styleText("cyan", ` → ${name}: building...`)) + await execAsync("npm run build", { cwd: pluginDir }) + } + await execAsync("npm prune --omit=dev", { cwd: pluginDir }) + linkPeerPlugins(pluginDir) + return true + } catch (error) { + console.log(styleText("red", ` ✗ ${name}: build failed`)) + return false + } +} + +/** + * Run async tasks with bounded concurrency. + * @param {Array} items - Items to process + * @param {number} concurrency - Max parallel tasks + * @param {Function} fn - Async function to run per item + * @returns {Promise} Results in order + */ +async function runParallel(items, concurrency, fn) { + const results = new Array(items.length) + let nextIndex = 0 + + async function worker() { + while (nextIndex < items.length) { + const i = nextIndex++ + results[i] = await fn(items[i], i) + } + } + + const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker()) + await Promise.all(workers) + return results +} + +/** + * Check whether a plugin's .gitignore excludes dist/. + * When dist/ is gitignored, the plugin cannot ship pre-built output in version + * control (e.g. because it uses tree-shaking) and must always be built locally. + */ +function isDistGitignored(pluginDir) { + const gitignorePath = path.join(pluginDir, ".gitignore") + if (!fs.existsSync(gitignorePath)) return false + + const lines = fs.readFileSync(gitignorePath, "utf-8").split("\n") + return lines.some((line) => { + const trimmed = line.trim() + return trimmed === "dist" || trimmed === "dist/" || trimmed === "/dist" || trimmed === "/dist/" + }) +} + +function hasPrebuiltDist(pluginDir) { + const distDir = path.join(pluginDir, "dist") + return fs.existsSync(distDir) && !isDistGitignored(pluginDir) +} + +function needsBuild(pluginDir) { + if (isDistGitignored(pluginDir)) return true + const distDir = path.join(pluginDir, "dist") + return !fs.existsSync(distDir) +} + +/** + * After pruning devDependencies, peerDependencies may no longer be installed + * in the plugin's own node_modules. This function resolves them: + * + * 1. @quartz-community/* peers → symlink to the co-installed sibling plugin + * 2. All other peers → symlink to the host Quartz node_modules so plugins + * share a single copy of packages like unified, vfile, rehype-raw, etc. + */ +function trySymlink(target, linkPath) { + symlinkOrCopySync(target, linkPath) +} + +function linkPeerPlugins(pluginDir) { + const pkgPath = path.join(pluginDir, "package.json") + if (!fs.existsSync(pkgPath)) return + + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) + const peers = pkg.peerDependencies ?? {} + + const quartzRoot = path.resolve(pluginDir, "..", "..", "..") + const hostNodeModules = path.join(quartzRoot, "node_modules") + + for (const peerName of Object.keys(peers)) { + const peerNodeModulesPath = path.join(pluginDir, "node_modules", ...peerName.split("/")) + if (fs.existsSync(peerNodeModulesPath)) continue + + if (peerName.startsWith("@quartz-community/")) { + const siblingPlugin = findPluginByPackageName(peerName) + if (!siblingPlugin) continue + + const scopeDir = path.join(pluginDir, "node_modules", peerName.split("/")[0]) + fs.mkdirSync(scopeDir, { recursive: true }) + + const target = path.relative(scopeDir, siblingPlugin) + trySymlink(target, peerNodeModulesPath) + continue + } + + const hostPeerPath = path.join(hostNodeModules, ...peerName.split("/")) + if (!fs.existsSync(hostPeerPath)) continue + + const parts = peerName.split("/") + if (parts.length > 1) { + const scopeDir = path.join(pluginDir, "node_modules", parts[0]) + fs.mkdirSync(scopeDir, { recursive: true }) + } else { + fs.mkdirSync(path.join(pluginDir, "node_modules"), { recursive: true }) + } + + const target = path.relative(path.dirname(peerNodeModulesPath), hostPeerPath) + trySymlink(target, peerNodeModulesPath) + } +} + +/** + * Search installed plugins for one whose package.json "name" matches the given + * npm package name (e.g. "@quartz-community/bases-page"). + */ +function findPluginByPackageName(packageName) { + if (!fs.existsSync(PLUGINS_DIR)) return null + + const plugins = fs.readdirSync(PLUGINS_DIR).filter((entry) => { + const entryPath = path.join(PLUGINS_DIR, entry) + return fs.statSync(entryPath).isDirectory() + }) + + for (const pluginDirName of plugins) { + const pkgPath = path.join(PLUGINS_DIR, pluginDirName, "package.json") + if (!fs.existsSync(pkgPath)) continue + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) + if (pkg.name === packageName) { + return path.join(PLUGINS_DIR, pluginDirName) + } + } catch {} + } + return null +} + +const PLUGIN_TYPE_PATTERN = + /Quartz(?:Emitter|Transformer|Filter|PageType)Plugin|QuartzComponentConstructor|\(.*\)\s*=>\s*QuartzComponent\b/ + +function resolveOriginalName(exportName, dtsContent) { + const aliasPattern = new RegExp(`(\\w+)\\s+as\\s+${exportName}\\b`) + const match = dtsContent.match(aliasPattern) + return match ? match[1] : exportName +} + +function isOverridableExport(name, dtsContent) { + const declName = resolveOriginalName(name, dtsContent) + const declPattern = new RegExp(`declare\\s+const\\s+${declName}\\s*:\\s*(.+?)(?:;|$)`, "m") + const match = dtsContent.match(declPattern) + if (!match) return false + return PLUGIN_TYPE_PATTERN.test(match[1]) +} + +function parseExportsFromDts(content) { + const exports = [] + const exportMatches = content.matchAll(/export\s*{\s*([^}]+)\s*}(?:\s*from\s*['"]([^'"]+)['"])?/g) + for (const match of exportMatches) { + const fromModule = match[2] + if (fromModule?.startsWith("@")) continue + + const names = match[1] + .split(",") + .map((n) => n.trim()) + .filter(Boolean) + for (const name of names) { + const cleanName = name.split(" as ").pop()?.trim() || name.trim() + if (cleanName && !cleanName.startsWith("_") && !INTERNAL_EXPORTS.has(cleanName)) { + const finalName = cleanName.replace(/^type\s+/, "") + if (name.includes("type ")) { + exports.push(`type ${finalName}`) + } else { + exports.push(finalName) + } + } + } + } + return exports +} + +async function regeneratePluginIndex() { + if (!fs.existsSync(PLUGINS_DIR)) return + + const pluginDirs = fs.readdirSync(PLUGINS_DIR).filter((name) => { + const pluginPath = path.join(PLUGINS_DIR, name) + return fs.statSync(pluginPath).isDirectory() + }) + + // Phase 1: Collect all exports per plugin, detect conflicts + const pluginExports = new Map() + const nameCount = new Map() + + for (const pluginName of pluginDirs) { + const pluginDir = path.join(PLUGINS_DIR, pluginName) + const distIndex = path.join(pluginDir, "dist", "index.d.ts") + + if (!fs.existsSync(distIndex)) continue + + const dtsContent = fs.readFileSync(distIndex, "utf-8") + const exportedNames = parseExportsFromDts(dtsContent) + const named = exportedNames.filter((e) => !e.startsWith("type ")) + const types = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5)) + + const overridable = named.filter((n) => isOverridableExport(n, dtsContent)) + const passthrough = named.filter((n) => !isOverridableExport(n, dtsContent)) + + if (overridable.length > 0 || passthrough.length > 0 || types.length > 0) { + pluginExports.set(pluginName, { overridable, passthrough, types }) + for (const n of [...overridable, ...passthrough]) { + nameCount.set(n, (nameCount.get(n) ?? 0) + 1) + } + } + } + + // Phase 2: Generate index with registry import, plugin map, and conditional top-level exports + const lines = [] + + lines.push(`import { componentRegistry } from "../../quartz/components/registry"`) + lines.push("") + + // Type re-exports + for (const [pluginName, { types }] of pluginExports) { + if (types.length > 0) { + lines.push(`export type { ${types.join(", ")} } from "./${pluginName}"`) + } + } + + // Direct re-exports for non-overridable values (constants, utility functions, etc.) + for (const [pluginName, { passthrough }] of pluginExports) { + if (passthrough.length === 0) continue + const unique = passthrough.filter((n) => (nameCount.get(n) ?? 0) === 1) + if (unique.length > 0) { + lines.push(`export { ${unique.join(", ")} } from "./${pluginName}"`) + } + } + lines.push("") + + // Generate the plugins map with override wrappers (overridable exports only) + lines.push( + `export const plugins: Record void>> = {`, + ) + for (const [pluginName, { overridable }] of pluginExports) { + if (overridable.length === 0) continue + const escapedName = pluginName.replace(/"/g, '\\"') + lines.push(` "${escapedName}": {`) + for (const n of overridable) { + lines.push( + ` ${n}: (...args: unknown[]) => { componentRegistry.setOptionOverrides("${escapedName}", args[0] as Record); },`, + ) + } + lines.push(` },`) + } + lines.push(`}`) + lines.push("") + + // Top-level exports for overridable names: alias to the plugins map wrapper + for (const [pluginName, { overridable }] of pluginExports) { + if (overridable.length === 0) continue + + const unique = overridable.filter((n) => (nameCount.get(n) ?? 0) === 1) + const conflicting = overridable.filter((n) => (nameCount.get(n) ?? 0) > 1) + + if (unique.length > 0) { + const escapedName = pluginName.replace(/"/g, '\\"') + for (const n of unique) { + lines.push(`export const ${n} = plugins["${escapedName}"].${n}`) + } + } + + if (conflicting.length > 0) { + for (const n of conflicting) { + console.warn( + styleText("yellow", `⚠`), + `Export "${n}" conflicts across plugins — use plugins["${pluginName}"].${n} in quartz.ts`, + ) + } + } + } + + lines.push("") + + const indexContent = lines.join("\n") + const indexPath = path.join(PLUGINS_DIR, "index.ts") + fs.writeFileSync(indexPath, indexContent) +} + +export async function handlePluginInstallUnified({ + names, + fromConfig = false, + latest = false, + clean = false, + dryRun = false, + concurrency: concurrencyOption, +} = {}) { + if (clean && latest) { + console.log(styleText("red", "✗ --clean and --latest cannot be used together")) + return + } + + const resolvedConcurrency = Math.max(1, concurrencyOption ?? os.cpus().length) + + const pluginsJson = readPluginsJson() + let lockfile = readLockfile() + + if (!fromConfig && !lockfile) { + console.log( + styleText("yellow", "⚠ No quartz.lock.json found. Run 'npx quartz plugin add ' first."), + ) + return + } + + const resolvedNames = names + ? names.map((name) => + resolveLockfileName(name, lockfile ?? { version: "1.0.0", plugins: {} }, pluginsJson), + ) + : null + const nameFilter = resolvedNames ? new Set(resolvedNames) : null + + if (dryRun && latest) { + if (!lockfile || Object.keys(lockfile.plugins).length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + const nameOverrides = getNameOverrides(lockfile, pluginsJson) + + const rows = Object.entries(lockfile.plugins) + .filter(([name]) => !nameFilter || nameFilter.has(name)) + .map(([name, entry]) => ({ + name, + entry, + displayName: nameOverrides.get(name) ?? name, + })) + + const isTTY = process.stdout.isTTY + const nameWidth = Math.max(6, ...rows.map((row) => row.displayName.length)) + 2 + const header = `${"Plugin".padEnd(nameWidth)}${"Installed".padEnd(12)}${"Latest".padEnd(12)}Status` + + const renderRow = ({ displayName }, installed, latest, statusLabel) => + `${displayName.padEnd(nameWidth)}${installed.padEnd(12)}${latest.padEnd(12)}${statusLabel}` + + const updateRow = (index, installed, latest, statusLabel) => { + if (!isTTY) return + const offset = rows.length - index + process.stdout.write( + `\x1b[${offset}A\x1b[2K\r${renderRow(rows[index], installed, latest, statusLabel)}\x1b[${offset}B`, + ) + } + + if (isTTY) { + console.log(styleText("bold", "Checking for plugin updates...\n")) + console.log(styleText("bold", header)) + console.log("─".repeat(header.length)) + for (const row of rows) { + if (row.entry.commit === "local") { + console.log(renderRow(row, "local", "—", styleText("green", "local"))) + continue + } + console.log(renderRow(row, row.entry.commit.slice(0, 7), "—", styleText("cyan", "⋯"))) + } + } + + const promises = rows.map((row, index) => { + if (row.entry.commit === "local") { + return Promise.resolve({ + index, + installed: "local", + latest: "—", + status: "local", + }) + } + + const lsRemoteRef = row.entry.ref ? `refs/heads/${row.entry.ref}` : "HEAD" + return execAsync(`git ls-remote "${row.entry.resolved}" ${lsRemoteRef}`) + .then(({ stdout }) => { + const latestCommit = stdout.split("\t")[0].trim() + const isCurrent = latestCommit === row.entry.commit + const installed = row.entry.commit.slice(0, 7) + const latest = latestCommit.slice(0, 7) + const statusLabel = isCurrent + ? styleText("green", "up to date") + : styleText("yellow", "update available") + updateRow(index, installed, latest, statusLabel) + return { + index, + installed, + latest, + status: isCurrent ? "up to date" : "update available", + } + }) + .catch(() => { + const installed = row.entry.commit.slice(0, 7) + const latest = "?" + const statusLabel = styleText("red", "check failed") + updateRow(index, installed, latest, statusLabel) + return { + index, + installed, + latest, + status: "check failed", + } + }) + }) + + const results = await Promise.all(promises) + + if (!isTTY) { + console.log(styleText("bold", "Checking for plugin updates...\n")) + console.log(styleText("bold", header)) + console.log("─".repeat(header.length)) + for (const { index, installed, latest, status } of results) { + const color = + status === "up to date" || status === "local" + ? "green" + : status === "check failed" + ? "red" + : "yellow" + console.log(renderRow(rows[index], installed, latest, styleText(color, status))) + } + } + return + } + + if (fromConfig) { + if (!pluginsJson?.plugins || pluginsJson.plugins.length === 0) { + console.log(styleText("gray", "No plugins configured")) + return + } + + if (!lockfile) { + lockfile = { version: "1.0.0", plugins: {} } + } + + if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }) + } + + const configNames = new Set(pluginsJson.plugins.map((entry) => extractPluginName(entry.source))) + const orphans = Object.keys(lockfile.plugins).filter((name) => !configNames.has(name)) + + const missing = pluginsJson.plugins + .filter((entry) => { + const name = extractPluginName(entry.source) + const pluginDir = path.join(PLUGINS_DIR, name) + if (lockfile.plugins[name] && fs.existsSync(pluginDir)) return false + const src = getSourceUrl(entry.source) + return ( + src.startsWith("github:") || + src.startsWith("git+") || + src.startsWith("https://") || + isLocalSource(src) + ) + }) + .filter((entry) => { + if (!nameFilter) return true + const name = extractPluginName(entry.source) + return nameFilter.has(name) + }) + + if (missing.length === 0) { + console.log(styleText("green", "✓ All configured plugins are already installed")) + if (dryRun) { + if (orphans.length > 0) { + console.log() + console.log(`Found ${orphans.length} orphaned plugin(s) in lockfile:\n`) + for (const name of orphans) { + console.log(` ${styleText("yellow", name)} — in lockfile but not in config`) + } + console.log() + console.log( + styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to resolve."), + ) + } + return + } + if (orphans.length === 0) { + return + } + } + + if (missing.length > 0) { + console.log(`Found ${missing.length} uninstalled plugin(s) in config:\n`) + for (const entry of missing) { + const name = extractPluginName(entry.source) + console.log(` ${styleText("yellow", name)} — ${formatSource(entry.source)}`) + } + console.log() + + if (dryRun) { + if (orphans.length > 0) { + console.log(`Found ${orphans.length} orphaned plugin(s) in lockfile:\n`) + for (const name of orphans) { + console.log(` ${styleText("yellow", name)} — in lockfile but not in config`) + } + console.log() + } + console.log( + styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to resolve."), + ) + return + } + } + + const installed = [] + let failed = 0 + let lockfileChanged = false + + // Handle existing dirs and local symlinks (fast), collect remote clones + const remoteEntries = [] + for (const entry of missing) { + try { + const { name, url, ref, local, subdir } = parseGitSource(entry.source) + const pluginDir = path.join(PLUGINS_DIR, name) + + if (fs.existsSync(pluginDir)) { + if (local) { + console.log( + styleText("yellow", `⚠ ${name} directory already exists, updating lockfile`), + ) + lockfile.plugins[name] = { + source: entry.source, + resolved: url, + commit: "local", + ...(subdir && { subdir }), + installedAt: new Date().toISOString(), + } + installed.push({ name, pluginDir }) + lockfileChanged = true + continue + } + console.log(styleText("yellow", `⚠ ${name} directory already exists, updating lockfile`)) + const commit = getGitCommit(pluginDir) + lockfile.plugins[name] = { + source: entry.source, + resolved: url, + commit, + ...(ref && { ref }), + ...(subdir && { subdir }), + installedAt: new Date().toISOString(), + } + installed.push({ name, pluginDir }) + lockfileChanged = true + continue + } + + if (local) { + let resolvedPath = path.resolve(url) + if (subdir) resolvedPath = path.join(resolvedPath, subdir) + if (!fs.existsSync(resolvedPath)) { + console.log(styleText("red", `✗ Local path does not exist: ${resolvedPath}`)) + failed++ + continue + } + console.log(styleText("cyan", `→ Linking ${name} from ${resolvedPath}...`)) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + symlinkOrCopySync(resolvedPath, pluginDir) + lockfile.plugins[name] = { + source: entry.source, + resolved: resolvedPath, + commit: "local", + ...(subdir && { subdir }), + installedAt: new Date().toISOString(), + } + installed.push({ name, pluginDir }) + lockfileChanged = true + console.log(styleText("green", `✓ Linked ${name} (local)`)) + } else { + remoteEntries.push({ entry, name, url, ref, subdir, pluginDir }) + } + } catch (error) { + console.log(styleText("red", `✗ Failed to resolve ${formatSource(entry.source)}: ${error}`)) + failed++ + } + } + + // Clone remote plugins in parallel + if (remoteEntries.length > 0) { + const concurrency = resolvedConcurrency + await runParallel( + remoteEntries, + concurrency, + async ({ entry, name, url, ref, subdir, pluginDir }) => { + try { + if (subdir) { + console.log(styleText("cyan", `→ Cloning ${name} from ${url} (subdir: ${subdir})...`)) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + const commit = await cloneWithSubdirAsync({ url, ref, subdir, pluginDir }) + lockfile.plugins[name] = { + source: entry.source, + resolved: url, + commit, + ...(ref && { ref }), + subdir, + installedAt: new Date().toISOString(), + } + installed.push({ name, pluginDir }) + lockfileChanged = true + console.log( + styleText("green", `✓ Cloned ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`), + ) + } else { + console.log(styleText("cyan", `→ Cloning ${name} from ${url}...`)) + + const branchArg = ref ? ` --branch ${ref}` : "" + await execAsync(`git clone --depth 1${branchArg} "${url}" "${pluginDir}"`) + + const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir }) + const commit = stdout.trim() + lockfile.plugins[name] = { + source: entry.source, + resolved: url, + commit, + ...(ref && { ref }), + installedAt: new Date().toISOString(), + } + + installed.push({ name, pluginDir }) + lockfileChanged = true + console.log(styleText("green", `✓ Cloned ${name}@${commit.slice(0, 7)}`)) + } + } catch (error) { + console.log( + styleText("red", `✗ Failed to resolve ${formatSource(entry.source)}: ${error}`), + ) + failed++ + } + }, + ) + } + + if (installed.length > 0) { + console.log() + console.log(styleText("cyan", "→ Building plugins...")) + const concurrency = resolvedConcurrency + const results = await runParallel(installed, concurrency, async ({ name, pluginDir }) => { + const ok = await buildPluginAsync(pluginDir, name) + if (ok) console.log(styleText("green", ` ✓ ${name} built`)) + return ok + }) + for (const ok of results) { + if (!ok) failed++ + } + await regeneratePluginIndex() + } + + if (orphans.length > 0) { + console.log() + let removedOrphans = false + for (const name of orphans) { + const entry = lockfile.plugins[name] + if (entry?.commit === "local") { + console.log( + styleText( + "yellow", + `⚠ ${name} is a local plugin not in config — skipping (remove manually with 'plugin remove')`, + ), + ) + continue + } + const pluginDir = path.join(PLUGINS_DIR, name) + if (fs.existsSync(pluginDir)) { + fs.rmSync(pluginDir, { recursive: true }) + } + delete lockfile.plugins[name] + lockfileChanged = true + removedOrphans = true + console.log(styleText("yellow", `✗ Removed ${name} (not in config)`)) + } + if (removedOrphans) { + await regeneratePluginIndex() + } + } + + if (lockfileChanged) { + writeLockfile(lockfile) + console.log() + if (failed === 0) { + console.log(styleText("green", `✓ Resolved ${installed.length} plugin(s)`)) + } else { + console.log( + styleText("yellow", `⚠ Resolved ${installed.length} plugin(s), ${failed} failed`), + ) + } + console.log(styleText("gray", "Updated quartz.lock.json")) + } else if (failed > 0) { + console.log() + console.log(styleText("yellow", `⚠ Resolved ${installed.length} plugin(s), ${failed} failed`)) + } + + return + } + + if (dryRun) { + const entries = Object.entries(lockfile.plugins).filter(([name]) => + nameFilter ? nameFilter.has(name) : true, + ) + if (entries.length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + console.log(styleText("cyan", "→ Dry run: plugins to install from lockfile...")) + for (const [name, entry] of entries) { + const sourceLabel = entry.source ? formatSource(entry.source) : entry.resolved + const commitLabel = entry.commit === "local" ? "local" : entry.commit.slice(0, 7) + console.log(` ${styleText("yellow", name)} — ${sourceLabel} (${commitLabel})`) + } + return + } + + if (clean) { + console.log(styleText("cyan", "→ Restoring plugins from lockfile...")) + console.log() + + if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }) + } + + let installed = 0 + let failed = 0 + const restoredPlugins = [] + + const entries = Object.entries(lockfile.plugins).filter(([name]) => + nameFilter ? nameFilter.has(name) : true, + ) + + // Handle local symlinks and collect remote plugins to clone + const remotePlugins = [] + for (const [name, entry] of entries) { + const pluginDir = path.join(PLUGINS_DIR, name) + + if (fs.existsSync(pluginDir)) { + console.log(styleText("yellow", `⚠ ${name}: directory exists, skipping`)) + continue + } + + if (entry.commit === "local") { + try { + if (!fs.existsSync(entry.resolved)) { + console.log(styleText("red", ` ✗ ${name}: local path missing: ${entry.resolved}`)) + failed++ + continue + } + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + symlinkOrCopySync(entry.resolved, pluginDir) + console.log(styleText("green", `✓ ${name} restored (local symlink)`)) + restoredPlugins.push({ name, pluginDir }) + installed++ + } catch { + console.log(styleText("red", `✗ ${name}: failed to restore local symlink`)) + failed++ + } + continue + } + + remotePlugins.push({ name, entry, pluginDir }) + } + + // Clone remote plugins in parallel + if (remotePlugins.length > 0) { + const concurrency = resolvedConcurrency + await runParallel(remotePlugins, concurrency, async ({ name, entry, pluginDir }) => { + try { + if (entry.subdir) { + console.log( + styleText( + "cyan", + `→ ${name}: cloning ${entry.resolved}@${entry.commit.slice(0, 7)} (subdir: ${entry.subdir})...`, + ), + ) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + await cloneWithSubdirAsync({ + url: entry.resolved, + ref: entry.ref, + subdir: entry.subdir, + pluginDir, + }) + } else { + console.log( + styleText( + "cyan", + `→ ${name}: cloning ${entry.resolved}@${entry.commit.slice(0, 7)}...`, + ), + ) + const branchArg = entry.ref ? ` --branch ${entry.ref}` : "" + await execAsync(`git clone --depth 1${branchArg} "${entry.resolved}" "${pluginDir}"`) + await execAsync(`git checkout ${entry.commit}`, { cwd: pluginDir }) + } + console.log(styleText("green", `✓ ${name} restored`)) + restoredPlugins.push({ name, pluginDir }) + installed++ + } catch { + console.log(styleText("red", `✗ ${name}: failed to restore`)) + failed++ + } + }) + } + + if (restoredPlugins.length > 0) { + console.log() + console.log(styleText("cyan", "→ Building restored plugins...")) + const concurrency = resolvedConcurrency + const results = await runParallel( + restoredPlugins, + concurrency, + async ({ name, pluginDir }) => { + const ok = await buildPluginAsync(pluginDir, name) + if (ok) console.log(styleText("green", ` ✓ ${name} built`)) + return ok + }, + ) + for (const ok of results) { + if (!ok) { + failed++ + installed-- + } + } + await regeneratePluginIndex() + } + + console.log() + if (failed === 0) { + console.log(styleText("green", `✓ Restored ${installed} plugin(s)`)) + } else { + console.log(styleText("yellow", `⚠ Restored ${installed} plugin(s), ${failed} failed`)) + } + return + } + + if (latest) { + const pluginsToUpdate = nameFilter ? Array.from(nameFilter) : Object.keys(lockfile.plugins) + const updatedPlugins = [] + let lockfileChanged = false + + // Phase 1: Validate and categorize plugins (fast, sequential) + const validPlugins = [] + for (const name of pluginsToUpdate) { + const entry = lockfile.plugins[name] + if (!entry) { + console.log(styleText("yellow", `⚠ ${name} is not installed`)) + continue + } + + const pluginDir = path.join(PLUGINS_DIR, name) + if (!fs.existsSync(pluginDir)) { + console.log( + styleText("yellow", `⚠ ${name} directory missing. Run 'npx quartz plugin install'.`), + ) + continue + } + + if (entry.commit === "local") { + console.log(styleText("cyan", `→ Rebuilding local plugin ${name}...`)) + updatedPlugins.push({ name, pluginDir }) + continue + } + + validPlugins.push({ name, pluginDir, entry }) + } + + // Phase 2: Fetch/update plugins in parallel + if (validPlugins.length > 0) { + const concurrency = resolvedConcurrency + await runParallel(validPlugins, concurrency, async ({ name, pluginDir, entry }) => { + try { + console.log(styleText("cyan", `→ Updating ${name}...`)) + + if (entry.subdir) { + fs.rmSync(pluginDir, { recursive: true }) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + const newCommit = await cloneWithSubdirAsync({ + url: entry.resolved, + ref: entry.ref, + subdir: entry.subdir, + pluginDir, + }) + if (needsBuild(pluginDir)) { + updatedPlugins.push({ name, pluginDir }) + } + if (newCommit !== entry.commit) { + entry.commit = newCommit + entry.installedAt = new Date().toISOString() + lockfileChanged = true + console.log( + styleText( + "green", + `✓ Updated ${name} to ${newCommit.slice(0, 7)} (subdir: ${entry.subdir})`, + ), + ) + } else { + console.log(styleText("gray", `✓ ${name} rebuilt (subdir: ${entry.subdir})`)) + } + } else { + const fetchRef = entry.ref || "" + const resetTarget = entry.ref ? `origin/${entry.ref}` : "origin/HEAD" + await execAsync(`git fetch --depth 1 origin${fetchRef ? " " + fetchRef : ""}`, { + cwd: pluginDir, + }) + await execAsync(`git reset --hard ${resetTarget}`, { cwd: pluginDir }) + + const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir }) + const newCommit = stdout.trim() + if (newCommit !== entry.commit) { + entry.commit = newCommit + entry.installedAt = new Date().toISOString() + updatedPlugins.push({ name, pluginDir }) + lockfileChanged = true + console.log(styleText("green", `✓ Updated ${name} to ${newCommit.slice(0, 7)}`)) + } else { + console.log(styleText("gray", `✓ ${name} already up to date`)) + } + } + } catch (error) { + console.log(styleText("red", `✗ Failed to update ${name}: ${error}`)) + } + }) + } + + // Phase 3: Build updated plugins in parallel + if (updatedPlugins.length > 0) { + console.log() + console.log(styleText("cyan", "→ Rebuilding updated plugins...")) + const concurrency = resolvedConcurrency + await runParallel(updatedPlugins, concurrency, async ({ name, pluginDir }) => { + const ok = await buildPluginAsync(pluginDir, name) + if (ok) console.log(styleText("green", ` ✓ ${name} rebuilt`)) + return ok + }) + await regeneratePluginIndex() + } + + if (lockfileChanged) { + writeLockfile(lockfile) + console.log() + console.log(styleText("gray", "Updated quartz.lock.json")) + } + return + } + + if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }) + } + + const entries = Object.entries(lockfile.plugins).filter(([name]) => + nameFilter ? nameFilter.has(name) : true, + ) + if (entries.length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + console.log(styleText("cyan", "→ Installing plugins from lockfile...")) + let installed = 0 + let failed = 0 + const pluginsToBuild = [] + + // Handle local plugins and collect entries needing git operations + const gitEntries = [] + for (const [name, entry] of entries) { + const pluginDir = path.join(PLUGINS_DIR, name) + + if (entry.commit === "local") { + try { + if (fs.existsSync(pluginDir)) { + const stat = fs.lstatSync(pluginDir) + if (stat.isSymbolicLink() && fs.readlinkSync(pluginDir) === entry.resolved) { + console.log(styleText("gray", ` ✓ ${name} (local) already linked`)) + installed++ + continue + } + if (stat.isSymbolicLink()) fs.unlinkSync(pluginDir) + else fs.rmSync(pluginDir, { recursive: true }) + } + if (!fs.existsSync(entry.resolved)) { + console.log(styleText("red", ` ✗ ${name}: local path missing: ${entry.resolved}`)) + failed++ + continue + } + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + symlinkOrCopySync(entry.resolved, pluginDir) + console.log(styleText("green", ` ✓ ${name} (local) linked`)) + pluginsToBuild.push({ name, pluginDir }) + installed++ + } catch { + console.log(styleText("red", ` ✗ ${name}: failed to link local path`)) + failed++ + } + continue + } + + if (fs.existsSync(pluginDir)) { + if (entry.subdir) { + if (!needsBuild(pluginDir)) { + console.log( + styleText("gray", ` ✓ ${name}@${entry.commit.slice(0, 7)} already installed (subdir)`), + ) + installed++ + continue + } + pluginsToBuild.push({ name, pluginDir }) + installed++ + } else { + const currentCommit = getGitCommit(pluginDir) + if (currentCommit === entry.commit && !needsBuild(pluginDir)) { + console.log( + styleText("gray", ` ✓ ${name}@${entry.commit.slice(0, 7)} already installed`), + ) + installed++ + continue + } + if (currentCommit !== entry.commit) { + gitEntries.push({ name, entry, pluginDir, action: "update" }) + } else { + pluginsToBuild.push({ name, pluginDir }) + installed++ + } + } + } else { + gitEntries.push({ name, entry, pluginDir, action: "clone" }) + } + } + + // Run git fetch/clone operations in parallel + if (gitEntries.length > 0) { + const concurrency = resolvedConcurrency + await runParallel(gitEntries, concurrency, async ({ name, entry, pluginDir, action }) => { + try { + if (action === "update") { + console.log(styleText("cyan", ` → ${name}: updating to ${entry.commit.slice(0, 7)}...`)) + const fetchRef = entry.ref ? ` ${entry.ref}` : "" + await execAsync(`git fetch --depth 1 origin${fetchRef}`, { cwd: pluginDir }) + await execAsync(`git reset --hard ${entry.commit}`, { cwd: pluginDir }) + pluginsToBuild.push({ name, pluginDir }) + installed++ + } else { + if (entry.subdir) { + console.log(styleText("cyan", ` → ${name}: cloning (subdir: ${entry.subdir})...`)) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + await cloneWithSubdirAsync({ + url: entry.resolved, + ref: entry.ref, + subdir: entry.subdir, + pluginDir, + }) + } else { + console.log(styleText("cyan", ` → ${name}: cloning...`)) + const branchArg = entry.ref ? ` --branch ${entry.ref}` : "" + await execAsync(`git clone --depth 1${branchArg} "${entry.resolved}" "${pluginDir}"`) + if (entry.commit !== "unknown") { + await execAsync(`git fetch --depth 1 origin ${entry.commit}`, { cwd: pluginDir }) + await execAsync(`git checkout ${entry.commit}`, { cwd: pluginDir }) + } + } + console.log(styleText("green", ` ✓ ${name}@${entry.commit.slice(0, 7)}`)) + pluginsToBuild.push({ name, pluginDir }) + installed++ + } + } catch { + console.log( + styleText("red", ` ✗ ${name}: failed to ${action === "update" ? "update" : "clone"}`), + ) + failed++ + } + }) + } + + if (pluginsToBuild.length > 0) { + console.log() + console.log(styleText("cyan", "→ Building plugins...")) + const concurrency = resolvedConcurrency + const results = await runParallel(pluginsToBuild, concurrency, async ({ name, pluginDir }) => { + const ok = await buildPluginAsync(pluginDir, name) + if (ok) console.log(styleText("green", ` ✓ ${name} built`)) + return ok + }) + for (const ok of results) { + if (!ok) { + failed++ + installed-- + } + } + } + + await regeneratePluginIndex() + + console.log() + if (failed === 0) { + console.log(styleText("green", `✓ Installed ${installed} plugin(s)`)) + } else { + console.log(styleText("yellow", `⚠ Installed ${installed} plugin(s), ${failed} failed`)) + } +} + +export async function handlePluginInstall() { + return handlePluginInstallUnified() +} + +export async function handlePluginAdd( + sources, + { name: nameOverride, subdir: subdirOverride, concurrency: concurrencyOption } = {}, +) { + if (nameOverride && sources.length > 1) { + console.log(styleText("red", "✗ --name/--as can only be used when adding a single plugin")) + return + } + if (subdirOverride && sources.length > 1) { + console.log(styleText("red", "✗ --subdir can only be used when adding a single plugin")) + return + } + + const resolvedConcurrency = Math.max(1, concurrencyOption ?? os.cpus().length) + + let lockfile = readLockfile() + if (!lockfile) { + lockfile = { version: "1.0.0", plugins: {} } + } + + if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }) + } + + const addedPlugins = [] + + // Handle local plugins and collect remote sources to clone + const remoteSources = [] + for (const source of sources) { + try { + const parsed = parseGitSource(source) + const name = nameOverride ?? parsed.name + const url = parsed.url + const ref = parsed.ref + const local = parsed.local + const subdir = subdirOverride ?? parsed.subdir + const pluginDir = path.join(PLUGINS_DIR, name) + + let configSource = undefined + if (nameOverride || subdirOverride) { + configSource = { repo: source } + if (nameOverride) configSource.name = nameOverride + if (subdirOverride) configSource.subdir = subdirOverride + } + + if (fs.existsSync(pluginDir)) { + console.log(styleText("yellow", `⚠ ${name} already exists. Use 'update' to refresh.`)) + continue + } + + if (local) { + let resolvedPath = path.resolve(url) + if (subdir) resolvedPath = path.join(resolvedPath, subdir) + if (!fs.existsSync(resolvedPath)) { + console.log(styleText("red", `✗ Local path does not exist: ${resolvedPath}`)) + continue + } + console.log(styleText("cyan", `→ Adding ${name} from local path ${resolvedPath}...`)) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + symlinkOrCopySync(resolvedPath, pluginDir) + lockfile.plugins[name] = { + source, + resolved: resolvedPath, + commit: "local", + ...(subdir && { subdir }), + installedAt: new Date().toISOString(), + } + addedPlugins.push({ name, pluginDir, source, configSource }) + console.log(styleText("green", `✓ Added ${name} (local symlink)`)) + } else { + remoteSources.push({ source, name, url, ref, subdir, pluginDir, configSource }) + } + } catch (error) { + console.log(styleText("red", `✗ Failed to add ${formatSource(source)}: ${error}`)) + } + } + + // Clone remote plugins in parallel + if (remoteSources.length > 0) { + const concurrency = resolvedConcurrency + await runParallel( + remoteSources, + concurrency, + async ({ source, name, url, ref, subdir, pluginDir, configSource }) => { + try { + if (subdir) { + console.log(styleText("cyan", `→ Adding ${name} from ${url} (subdir: ${subdir})...`)) + fs.mkdirSync(path.dirname(pluginDir), { recursive: true }) + const commit = await cloneWithSubdirAsync({ url, ref, subdir, pluginDir }) + lockfile.plugins[name] = { + source, + resolved: url, + commit, + ...(ref && { ref }), + subdir, + installedAt: new Date().toISOString(), + } + addedPlugins.push({ name, pluginDir, source, configSource }) + console.log( + styleText("green", `✓ Added ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`), + ) + } else { + console.log(styleText("cyan", `→ Adding ${name} from ${url}...`)) + + const branchArg = ref ? ` --branch ${ref}` : "" + await execAsync(`git clone --depth 1${branchArg} "${url}" "${pluginDir}"`) + + const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir }) + const commit = stdout.trim() + lockfile.plugins[name] = { + source, + resolved: url, + commit, + ...(ref && { ref }), + installedAt: new Date().toISOString(), + } + + addedPlugins.push({ name, pluginDir, source, configSource }) + console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`)) + } + } catch (error) { + console.log(styleText("red", `✗ Failed to add ${formatSource(source)}: ${error}`)) + } + }, + ) + } + + if (addedPlugins.length > 0) { + console.log() + console.log(styleText("cyan", "→ Building plugins...")) + const concurrency = resolvedConcurrency + await runParallel(addedPlugins, concurrency, async ({ name, pluginDir }) => { + const ok = await buildPluginAsync(pluginDir, name) + if (ok) console.log(styleText("green", ` ✓ ${name} built`)) + return ok + }) + await regeneratePluginIndex() + } + + writeLockfile(lockfile) + const pluginsJson = readPluginsJson() + if (pluginsJson?.plugins) { + for (const { pluginDir, source, configSource } of addedPlugins) { + const manifest = readManifestFromPackageJson(pluginDir) + const newEntry = { + source: configSource ?? source, + enabled: manifest?.defaultEnabled ?? true, + options: manifest?.defaultOptions ?? {}, + order: manifest?.defaultOrder ?? 50, + } + + if (manifest?.components) { + const layoutPositions = new Set(["left", "right", "beforeBody", "afterBody"]) + const firstComponentKey = Object.keys(manifest.components)[0] + const comp = manifest.components[firstComponentKey] + if (comp?.defaultPosition && layoutPositions.has(comp.defaultPosition)) { + newEntry.layout = { + position: comp.defaultPosition, + priority: comp.defaultPriority ?? 50, + } + } + } + + pluginsJson.plugins.push(newEntry) + } + writePluginsJson(pluginsJson) + } + console.log() + console.log(styleText("gray", "Updated quartz.lock.json")) +} + +export async function handlePluginRemove(names) { + const lockfile = readLockfile() + if (!lockfile) { + console.log(styleText("yellow", "⚠ No plugins installed")) + return + } + + const pluginsJson = readPluginsJson() + let removed = false + const resolvedNames = [] + for (const name of names) { + const lockKey = resolveLockfileName(name, lockfile, pluginsJson) + resolvedNames.push(lockKey) + const pluginDir = path.join(PLUGINS_DIR, lockKey) + + if (!lockfile.plugins[lockKey] && !fs.existsSync(pluginDir)) { + console.log(styleText("yellow", `⚠ ${name} is not installed`)) + continue + } + + const displayName = lockKey !== name ? `${name} (${lockKey})` : name + console.log(styleText("cyan", `→ Removing ${displayName}...`)) + + if (fs.existsSync(pluginDir)) { + fs.rmSync(pluginDir, { recursive: true }) + } + + delete lockfile.plugins[lockKey] + console.log(styleText("green", `✓ Removed ${displayName}`)) + removed = true + } + + if (removed) { + await regeneratePluginIndex() + } + + writeLockfile(lockfile) + if (pluginsJson?.plugins) { + pluginsJson.plugins = pluginsJson.plugins.filter( + (plugin) => + !names.includes(extractPluginName(plugin.source)) && + !names.includes(formatSource(plugin.source)) && + !resolvedNames.includes(extractPluginName(plugin.source)), + ) + writePluginsJson(pluginsJson) + } + console.log() + console.log(styleText("gray", "Updated quartz.lock.json")) +} + +export async function handlePluginEnable(names) { + const json = readPluginsJson() + if (!json) { + console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot enable plugins.")) + return + } + + for (const name of names) { + const entry = json.plugins.find( + (e) => extractPluginName(e.source) === name || formatSource(e.source) === name, + ) + if (!entry) { + console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`)) + continue + } + if (entry.enabled) { + console.log(styleText("gray", `✓ ${name} is already enabled`)) + continue + } + entry.enabled = true + console.log(styleText("green", `✓ Enabled ${name}`)) + } + + writePluginsJson(json) +} + +export async function handlePluginDisable(names) { + const json = readPluginsJson() + if (!json) { + console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot disable plugins.")) + return + } + + for (const name of names) { + const entry = json.plugins.find( + (e) => extractPluginName(e.source) === name || formatSource(e.source) === name, + ) + if (!entry) { + console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`)) + continue + } + if (!entry.enabled) { + console.log(styleText("gray", `✓ ${name} is already disabled`)) + continue + } + entry.enabled = false + console.log(styleText("green", `✓ Disabled ${name}`)) + } + + writePluginsJson(json) +} + +export async function handlePluginConfig(name, options = {}) { + const json = readPluginsJson() + if (!json) { + console.log(styleText("red", "✗ No quartz.config.yaml found.")) + return + } + + const entry = json.plugins.find( + (e) => extractPluginName(e.source) === name || formatSource(e.source) === name, + ) + if (!entry) { + console.log(styleText("red", `✗ Plugin "${name}" not found in quartz.config.yaml`)) + return + } + + if (options.set) { + const eqIndex = options.set.indexOf("=") + if (eqIndex === -1) { + console.log(styleText("red", "✗ Invalid format. Use: --set key=value")) + return + } + const key = options.set.slice(0, eqIndex) + let value = options.set.slice(eqIndex + 1) + + try { + value = JSON.parse(value) + } catch {} + + if (!entry.options) entry.options = {} + entry.options[key] = value + writePluginsJson(json) + console.log(styleText("green", `✓ Set ${name}.${key} = ${JSON.stringify(value)}`)) + } else { + console.log(styleText("bold", `Plugin: ${name}`)) + console.log(` Source: ${formatSource(entry.source)}`) + console.log(` Enabled: ${entry.enabled}`) + console.log(` Order: ${entry.order ?? 50}`) + if (entry.options && Object.keys(entry.options).length > 0) { + console.log(` Options:`) + for (const [k, v] of Object.entries(entry.options)) { + console.log(` ${k}: ${JSON.stringify(v)}`) + } + } else { + console.log(` Options: (none)`) + } + if (entry.layout) { + console.log(` Layout:`) + for (const [k, v] of Object.entries(entry.layout)) { + console.log(` ${k}: ${JSON.stringify(v)}`) + } + } + } +} + +export async function handlePluginCheck() { + return handlePluginInstallUnified({ latest: true, dryRun: true }) +} + +export async function handlePluginUpdate(names) { + return handlePluginInstallUnified({ names, latest: true }) +} + +export async function handlePluginList() { + const lockfile = readLockfile() + if (!lockfile || Object.keys(lockfile.plugins).length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + const pluginsJson = readPluginsJson() + const nameOverrides = getNameOverrides(lockfile, pluginsJson) + + console.log(styleText("bold", "Installed Plugins:")) + console.log() + + for (const [name, entry] of Object.entries(lockfile.plugins)) { + const pluginDir = path.join(PLUGINS_DIR, name) + const exists = fs.existsSync(pluginDir) + const overriddenName = nameOverrides.get(name) + const displayLabel = overriddenName + ? `${overriddenName} ${styleText("gray", `(dir: ${name})`)}` + : name + + if (entry.commit === "local") { + const isLinked = exists && fs.lstatSync(pluginDir).isSymbolicLink() + const status = isLinked ? styleText("green", "✓") : styleText("red", "✗") + console.log(` ${status} ${styleText("bold", displayLabel)}`) + console.log(` Source: ${formatSource(entry.source)}`) + console.log(` Type: local symlink`) + console.log(` Target: ${entry.resolved}`) + console.log(` Installed: ${new Date(entry.installedAt).toLocaleDateString()}`) + console.log() + continue + } + + let currentCommit = entry.commit + + if (exists) { + currentCommit = getGitCommit(pluginDir) + } + + const status = exists + ? currentCommit === entry.commit + ? styleText("green", "✓") + : styleText("yellow", "⚡") + : styleText("red", "✗") + + console.log(` ${status} ${styleText("bold", displayLabel)}`) + console.log(` Source: ${formatSource(entry.source)}`) + console.log(` Commit: ${entry.commit.slice(0, 7)}`) + if (currentCommit !== entry.commit && exists) { + console.log(` Current: ${currentCommit.slice(0, 7)} (modified)`) + } + console.log(` Installed: ${new Date(entry.installedAt).toLocaleDateString()}`) + console.log() + } +} + +export async function handlePluginStatus() { + const lockfile = readLockfile() + if (!lockfile || Object.keys(lockfile.plugins).length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + const pluginsJson = readPluginsJson() + const nameOverrides = getNameOverrides(lockfile, pluginsJson) + const enabledByName = new Map( + (pluginsJson?.plugins ?? []).map((entry) => [ + extractPluginName(entry.source), + entry.enabled !== false, + ]), + ) + + const rows = Object.entries(lockfile.plugins).map(([name, entry]) => { + const pluginDir = path.join(PLUGINS_DIR, name) + const exists = fs.existsSync(pluginDir) + const displayName = nameOverrides.get(name) ?? name + const sourceLabel = formatSource(entry.source) + const commitLabel = entry.commit === "local" ? "local" : `@${entry.commit.slice(0, 7)}` + const enabled = enabledByName.get(name) ?? false + return { name, entry, exists, displayName, sourceLabel, commitLabel, enabled } + }) + + const nameWidth = Math.max(8, ...rows.map((row) => row.displayName.length)) + 2 + const sourceWidth = Math.max(8, ...rows.map((row) => row.sourceLabel.length)) + 2 + const commitWidth = Math.max(6, ...rows.map((row) => row.commitLabel.length)) + 2 + const enabledWidth = Math.max("enabled".length, "disabled".length) + 2 + const updateWidth = + Math.max( + "— local".length, + "⋯".length, + "✓ up to date".length, + "↑ update available".length, + "✗ check failed".length, + ) + 2 + + const formatRow = (row, updateLabel, updateText) => { + const statusIcon = row.exists ? styleText("green", "✓") : styleText("red", "✗") + const enabledText = row.enabled ? "enabled" : "disabled" + const enabledLabel = row.enabled + ? styleText("green", enabledText) + : styleText("gray", enabledText) + const enabledColumn = `${enabledLabel}${" ".repeat(enabledWidth - enabledText.length)}` + const updateColumn = `${updateLabel}${" ".repeat(Math.max(0, updateWidth - updateText.length))}` + return ` ${statusIcon} ${row.displayName.padEnd(nameWidth)}${row.sourceLabel.padEnd( + sourceWidth, + )}${row.commitLabel.padEnd(commitWidth)}${enabledColumn}${updateColumn}` + } + + const updateDisplay = (status) => { + switch (status) { + case "local": + return { text: "— local", label: styleText("gray", "— local") } + case "up_to_date": + return { text: "✓ up to date", label: styleText("green", "✓ up to date") } + case "update_available": + return { text: "↑ update available", label: styleText("yellow", "↑ update available") } + case "failed": + return { text: "✗ check failed", label: styleText("red", "✗ check failed") } + default: + return { text: "⋯", label: styleText("cyan", "⋯") } + } + } + + const isTTY = process.stdout.isTTY + + const updateLine = (index, updateLabel, updateText) => { + if (!isTTY) return + const offset = rows.length - index + process.stdout.write( + `\x1b[${offset}A\x1b[2K\r${formatRow(rows[index], updateLabel, updateText)}\x1b[${offset}B`, + ) + } + + if (isTTY) { + console.log(styleText("bold", "Installed Plugins:")) + console.log() + for (const row of rows) { + const display = + row.entry.commit === "local" ? updateDisplay("local") : updateDisplay("checking") + console.log(formatRow(row, display.label, display.text)) + } + } + + const promises = rows.map((row, index) => { + if (row.entry.commit === "local") { + return Promise.resolve({ + index, + status: "local", + name: row.displayName, + }) + } + + const lsRemoteRef = row.entry.ref ? `refs/heads/${row.entry.ref}` : "HEAD" + return execAsync(`git ls-remote "${row.entry.resolved}" ${lsRemoteRef}`) + .then(({ stdout }) => { + const latestCommit = stdout.split("\t")[0].trim() + const status = latestCommit === row.entry.commit ? "up_to_date" : "update_available" + const display = updateDisplay(status) + updateLine(index, display.label, display.text) + return { index, status, name: row.displayName } + }) + .catch(() => { + const display = updateDisplay("failed") + updateLine(index, display.label, display.text) + return { index, status: "failed", name: row.displayName } + }) + }) + + const results = await Promise.all(promises) + const updatesAvailable = results + .filter((result) => result.status === "update_available") + .map((result) => result.name) + const failedChecks = results + .filter((result) => result.status === "failed") + .map((result) => result.name) + + if (!isTTY) { + console.log(styleText("bold", "Installed Plugins:")) + console.log() + for (const result of results) { + const row = rows[result.index] + const display = updateDisplay(result.status) + console.log(formatRow(row, display.label, display.text)) + } + } + + if (updatesAvailable.length === 0 && failedChecks.length === 0) { + console.log(styleText("green", "\n✓ All plugins up to date")) + return + } + + if (updatesAvailable.length > 0) { + console.log(styleText("yellow", `\nUpdates available: ${updatesAvailable.join(", ")}`)) + console.log(styleText("gray", "Run 'npx quartz plugin install --latest' to update.")) + } + + if (failedChecks.length > 0) { + console.log(styleText("red", `\nChecks failed: ${failedChecks.join(", ")}`)) + } +} + +export async function handlePluginRestore() { + return handlePluginInstallUnified({ clean: true }) +} + +export async function handlePluginPrune({ dryRun = false } = {}) { + const lockfile = readLockfile() + if (!lockfile || Object.keys(lockfile.plugins).length === 0) { + console.log(styleText("gray", "No plugins installed")) + return + } + + const pluginsJson = readPluginsJson() + const configuredNames = new Set( + (pluginsJson?.plugins ?? []).map((entry) => extractPluginName(entry.source)), + ) + + const orphans = Object.keys(lockfile.plugins).filter((name) => !configuredNames.has(name)) + + if (orphans.length === 0) { + console.log(styleText("green", "✓ No orphaned plugins found — nothing to prune")) + return + } + + console.log(`Found ${orphans.length} orphaned plugin(s):\n`) + for (const name of orphans) { + console.log(` ${styleText("yellow", name)} — in lockfile but not in config`) + } + console.log() + + if (dryRun) { + console.log(styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to prune.")) + return + } + + let removed = 0 + for (const name of orphans) { + const pluginDir = path.join(PLUGINS_DIR, name) + + console.log(styleText("cyan", `→ Removing ${name}...`)) + + if (fs.existsSync(pluginDir)) { + fs.rmSync(pluginDir, { recursive: true }) + } + + delete lockfile.plugins[name] + console.log(styleText("green", `✓ Removed ${name}`)) + removed++ + } + + if (removed > 0) { + await regeneratePluginIndex() + } + + writeLockfile(lockfile) + console.log() + console.log(styleText("green", `✓ Pruned ${removed} plugin(s)`)) + console.log(styleText("gray", "Updated quartz.lock.json")) +} + +export async function handlePluginResolve({ dryRun = false } = {}) { + return handlePluginInstallUnified({ fromConfig: true, dryRun }) +} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/templates/blog.yaml b/Local/storage/thlab-notes/worker/quartz/cli/templates/blog.yaml new file mode 100644 index 0000000..5ca9c0e --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/templates/blog.yaml @@ -0,0 +1,291 @@ +# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json +# Template: blog +# A blog-focused setup with recent notes and comments enabled. +configuration: + pageTitle: Quartz 5 + pageTitleSuffix: "" + enableSPA: true + enablePopovers: true + analytics: + provider: plausible + locale: en-US + baseUrl: quartz.jzhao.xyz + ignorePatterns: + - private + - templates + - .obsidian + theme: + fontOrigin: googleFonts + cdnCaching: true + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono + colors: + lightMode: + light: "#faf8f8" + lightgray: "#e5e5e5" + gray: "#b8b8b8" + darkgray: "#4e4e4e" + dark: "#2b2b2b" + secondary: "#284b63" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#fff23688" + darkMode: + light: "#161618" + lightgray: "#393639" + gray: "#646464" + darkgray: "#d4d4d4" + dark: "#ebebec" + secondary: "#7b97aa" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#b3aa0288" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + options: + enableInHtmlEmbed: false + enableCheckbox: true + order: 30 + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: github:quartz-community/crawl-links + enabled: true + options: + markdownLinkResolution: shortest + order: 60 + - source: github:quartz-community/description + enabled: true + order: 70 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: github:quartz-community/citations + enabled: false + order: 85 + - source: github:quartz-community/hard-line-breaks + enabled: false + order: 90 + - source: github:quartz-community/ox-hugo + enabled: false + order: 91 + - source: github:quartz-community/roam + enabled: false + order: 92 + - source: github:quartz-community/fonts + enabled: true + - source: github:quartz-community/remove-draft + enabled: true + - source: github:quartz-community/explicit-publish + enabled: false + - source: github:quartz-community/unlisted-pages + enabled: true + options: {} + order: 45 + - source: github:quartz-community/encrypted-pages + enabled: false + - source: github:quartz-community/stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + display: all + - source: github:quartz-community/alias-redirects + enabled: true + - source: github:quartz-community/content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: github:quartz-community/favicon + enabled: true + - source: github:quartz-community/og-image + enabled: true + - source: github:quartz-community/cname + enabled: true + - source: github:quartz-community/canvas-page + enabled: true + - source: github:quartz-community/content-page + enabled: true + - source: github:quartz-community/folder-page + enabled: true + - source: github:quartz-community/tag-page + enabled: true + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 50 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/page-title + enabled: true + layout: + position: left + priority: 10 + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: github:quartz-community/comments + enabled: true + options: + provider: giscus + options: + repo: "TODO:username/repo-name" + repoId: "TODO:your-repo-id" + category: Announcements + categoryId: "TODO:your-category-id" + mapping: url + strict: true + reactionsEnabled: true + inputPosition: bottom + lightTheme: light + darkTheme: dark + lang: en + layout: + position: afterBody + priority: 10 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: github:quartz-community/recent-notes + enabled: true + options: + title: Recent Notes + limit: 5 + linkToMore: false + showTags: true + layout: + position: left + priority: 25 + - source: github:quartz-community/spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: github:quartz-community/bases-page + enabled: true + options: {} + order: 50 + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: "---" + language: yaml + order: 5 + layout: + position: beforeBody + priority: 15 + display: all +layout: + groups: + toolbar: + priority: 35 + direction: row + gap: 0.5rem + byPageType: + "404": + positions: + beforeBody: [] + left: [] + right: [] + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + canvas: {} + bases: {} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/templates/default.yaml b/Local/storage/thlab-notes/worker/quartz/cli/templates/default.yaml new file mode 100644 index 0000000..111654b --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/templates/default.yaml @@ -0,0 +1,277 @@ +# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json +# Template: default +# A clean Quartz setup with sensible defaults. +configuration: + pageTitle: Quartz 5 + pageTitleSuffix: "" + enableSPA: true + enablePopovers: true + analytics: + provider: plausible + locale: en-US + baseUrl: quartz.jzhao.xyz + ignorePatterns: + - private + - templates + - .obsidian + theme: + fontOrigin: googleFonts + cdnCaching: true + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono + colors: + lightMode: + light: "#faf8f8" + lightgray: "#e5e5e5" + gray: "#b8b8b8" + darkgray: "#4e4e4e" + dark: "#2b2b2b" + secondary: "#284b63" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#fff23688" + darkMode: + light: "#161618" + lightgray: "#393639" + gray: "#646464" + darkgray: "#d4d4d4" + dark: "#ebebec" + secondary: "#7b97aa" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#b3aa0288" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + options: + enableInHtmlEmbed: false + enableCheckbox: true + order: 30 + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: github:quartz-community/crawl-links + enabled: true + options: + markdownLinkResolution: shortest + order: 60 + - source: github:quartz-community/description + enabled: true + order: 70 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: github:quartz-community/citations + enabled: false + order: 85 + - source: github:quartz-community/hard-line-breaks + enabled: false + order: 90 + - source: github:quartz-community/ox-hugo + enabled: false + order: 91 + - source: github:quartz-community/roam + enabled: false + order: 92 + - source: github:quartz-community/fonts + enabled: true + - source: github:quartz-community/remove-draft + enabled: true + - source: github:quartz-community/explicit-publish + enabled: false + - source: github:quartz-community/unlisted-pages + enabled: true + options: {} + order: 45 + - source: github:quartz-community/encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json + - source: github:quartz-community/stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + display: all + - source: github:quartz-community/alias-redirects + enabled: true + - source: github:quartz-community/content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: github:quartz-community/favicon + enabled: true + - source: github:quartz-community/og-image + enabled: true + - source: github:quartz-community/cname + enabled: true + - source: github:quartz-community/canvas-page + enabled: true + - source: github:quartz-community/content-page + enabled: true + - source: github:quartz-community/folder-page + enabled: true + - source: github:quartz-community/tag-page + enabled: true + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 50 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/page-title + enabled: true + layout: + position: left + priority: 10 + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: github:quartz-community/comments + enabled: false + options: + provider: giscus + options: {} + layout: + position: afterBody + priority: 10 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: github:quartz-community/recent-notes + enabled: false + - source: github:quartz-community/spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: github:quartz-community/bases-page + enabled: true + options: {} + order: 50 + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: "---" + language: yaml + order: 5 + layout: + position: beforeBody + priority: 15 + display: all +layout: + groups: + toolbar: + priority: 35 + direction: row + gap: 0.5rem + byPageType: + "404": + positions: + beforeBody: [] + left: [] + right: [] + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + canvas: {} + bases: {} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/templates/obsidian.yaml b/Local/storage/thlab-notes/worker/quartz/cli/templates/obsidian.yaml new file mode 100644 index 0000000..b45dfb3 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/templates/obsidian.yaml @@ -0,0 +1,302 @@ +# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json +# Template: obsidian +# Optimized for Obsidian vaults with full OFM support and shortest link resolution. +configuration: + pageTitle: Quartz 5 + pageTitleSuffix: "" + enableSPA: true + enablePopovers: true + analytics: + provider: plausible + locale: en-US + baseUrl: quartz.jzhao.xyz + ignorePatterns: + - private + - templates + - .obsidian + theme: + fontOrigin: googleFonts + cdnCaching: true + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono + colors: + lightMode: + light: "#faf8f8" + lightgray: "#e5e5e5" + gray: "#b8b8b8" + darkgray: "#4e4e4e" + dark: "#2b2b2b" + secondary: "#284b63" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#fff23688" + darkMode: + light: "#161618" + lightgray: "#393639" + gray: "#646464" + darkgray: "#d4d4d4" + dark: "#ebebec" + secondary: "#7b97aa" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#b3aa0288" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + options: + comments: true + highlight: true + wikilinks: true + callouts: true + mermaid: true + parseTags: true + parseArrows: true + parseBlockReferences: true + enableInHtmlEmbed: false + enableYouTubeEmbed: true + enableVideoEmbed: true + enableCheckbox: true + order: 30 + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: github:quartz-community/crawl-links + enabled: true + options: + markdownLinkResolution: shortest + # disableBrokenWikilinks: false # Set true to add a "broken" CSS class to internal links whose target is not in ctx.allSlugs. + order: 60 + - source: github:quartz-community/description + enabled: true + order: 70 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: github:quartz-community/citations + enabled: false + order: 85 + - source: github:quartz-community/hard-line-breaks + enabled: true + order: 90 + - source: github:quartz-community/ox-hugo + enabled: false + order: 91 + - source: github:quartz-community/roam + enabled: false + order: 92 + - source: github:quartz-community/fonts + enabled: true + - source: github:quartz-community/remove-draft + enabled: true + - source: github:quartz-community/explicit-publish + enabled: false + - source: github:quartz-community/unlisted-pages + enabled: true + options: {} + order: 45 + - source: github:quartz-community/encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json + - source: github:quartz-community/stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + display: all + - source: github:quartz-community/alias-redirects + enabled: true + - source: github:quartz-community/content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: github:quartz-community/favicon + enabled: true + - source: github:quartz-community/og-image + enabled: true + - source: github:quartz-community/cname + enabled: true + - source: github:quartz-community/canvas-page + enabled: true + - source: github:quartz-community/content-page + enabled: true + - source: github:quartz-community/folder-page + enabled: true + - source: github:quartz-community/tag-page + enabled: true + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 50 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/page-title + enabled: true + layout: + position: left + priority: 10 + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: github:quartz-community/comments + enabled: false + options: + provider: giscus + options: {} + layout: + position: afterBody + priority: 10 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: github:quartz-community/recent-notes + enabled: false + - source: github:quartz-community/spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: github:quartz-community/bases-page + enabled: true + options: {} + order: 50 + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: "---" + language: yaml + order: 5 + layout: + position: beforeBody + priority: 15 + display: all + - source: + name: quartz-themes + repo: github:saberzero1/quartz-themes + subdir: plugin + enabled: true + options: + theme: default + - source: github:quartz-community/obsidian-plugin-excalidraw + enabled: true + options: + enableInteraction: true + darkMode: auto + exportPadding: 20 + order: 50 +layout: + groups: + toolbar: + priority: 35 + direction: row + gap: 0.5rem + byPageType: + "404": + positions: + beforeBody: [] + left: [] + right: [] + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + canvas: {} + bases: {} diff --git a/Local/storage/thlab-notes/worker/quartz/cli/templates/ttrpg.yaml b/Local/storage/thlab-notes/worker/quartz/cli/templates/ttrpg.yaml new file mode 100644 index 0000000..2c06b76 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/cli/templates/ttrpg.yaml @@ -0,0 +1,308 @@ +# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json +# Template: ttrpg +# Obsidian-based setup with map plugin and ITS Theme for TTRPG/D&D wikis. +configuration: + pageTitle: Quartz 5 + pageTitleSuffix: "" + enableSPA: true + enablePopovers: true + analytics: + provider: plausible + locale: en-US + baseUrl: quartz.jzhao.xyz + ignorePatterns: + - private + - templates + - .obsidian + theme: + fontOrigin: googleFonts + cdnCaching: true + typography: + header: Schibsted Grotesk + body: Source Sans Pro + code: IBM Plex Mono + colors: + lightMode: + light: "#faf8f8" + lightgray: "#e5e5e5" + gray: "#b8b8b8" + darkgray: "#4e4e4e" + dark: "#2b2b2b" + secondary: "#284b63" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#fff23688" + darkMode: + light: "#161618" + lightgray: "#393639" + gray: "#646464" + darkgray: "#d4d4d4" + dark: "#ebebec" + secondary: "#7b97aa" + tertiary: "#84a59d" + highlight: rgba(143, 159, 169, 0.15) + textHighlight: "#b3aa0288" +plugins: + - source: github:quartz-community/created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: github:quartz-community/syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: github:quartz-community/obsidian-flavored-markdown + enabled: true + options: + comments: true + highlight: true + wikilinks: true + callouts: true + mermaid: true + parseTags: true + parseArrows: true + parseBlockReferences: true + enableInHtmlEmbed: false + enableYouTubeEmbed: true + enableVideoEmbed: true + enableCheckbox: true + order: 30 + - source: github:quartz-community/github-flavored-markdown + enabled: true + order: 40 + - source: github:quartz-community/table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: github:quartz-community/crawl-links + enabled: true + options: + markdownLinkResolution: shortest + # disableBrokenWikilinks: false # Set true to add a "broken" CSS class to internal links whose target is not in ctx.allSlugs. + order: 60 + - source: github:quartz-community/description + enabled: true + order: 70 + - source: github:quartz-community/latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: github:quartz-community/citations + enabled: false + order: 85 + - source: github:quartz-community/hard-line-breaks + enabled: true + order: 90 + - source: github:quartz-community/ox-hugo + enabled: false + order: 91 + - source: github:quartz-community/roam + enabled: false + order: 92 + - source: github:quartz-community/fonts + enabled: true + - source: github:quartz-community/remove-draft + enabled: true + - source: github:quartz-community/explicit-publish + enabled: false + - source: github:quartz-community/unlisted-pages + enabled: true + options: {} + order: 45 + - source: github:quartz-community/encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json + - source: github:quartz-community/stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + display: all + - source: github:quartz-community/alias-redirects + enabled: true + - source: github:quartz-community/content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: github:quartz-community/favicon + enabled: true + - source: github:quartz-community/og-image + enabled: true + - source: github:quartz-community/cname + enabled: true + - source: github:quartz-community/canvas-page + enabled: true + - source: github:quartz-community/content-page + enabled: true + - source: github:quartz-community/folder-page + enabled: true + - source: github:quartz-community/tag-page + enabled: true + - source: github:quartz-community/explorer + enabled: true + layout: + position: left + priority: 50 + - source: github:quartz-community/graph + enabled: true + layout: + position: right + priority: 10 + - source: github:quartz-community/search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: github:quartz-community/backlinks + enabled: true + layout: + position: right + priority: 50 + - source: github:quartz-community/article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: github:quartz-community/content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: github:quartz-community/tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: github:quartz-community/page-title + enabled: true + layout: + position: left + priority: 10 + - source: github:quartz-community/darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: github:quartz-community/reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: github:quartz-community/breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: github:quartz-community/comments + enabled: false + options: + provider: giscus + options: {} + layout: + position: afterBody + priority: 10 + - source: github:quartz-community/footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: github:quartz-community/recent-notes + enabled: false + - source: github:quartz-community/spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: github:quartz-community/bases-page + enabled: true + options: {} + order: 50 + - source: github:quartz-community/note-properties + enabled: true + options: + includeAll: false + includedProperties: + - description + - tags + - aliases + excludedProperties: [] + hidePropertiesView: false + delimiters: "---" + language: yaml + order: 5 + layout: + position: beforeBody + priority: 15 + display: all + # TTRPG-specific plugins + - source: github:Requiae/quartz-leaflet-bases-plugin + enabled: true + options: + enableCopyTool: false + - source: + name: quartz-themes + repo: github:saberzero1/quartz-themes + subdir: plugin + enabled: true + options: + theme: its-theme + variation: ttrpg-dnd + - source: github:quartz-community/obsidian-plugin-excalidraw + enabled: true + options: + enableInteraction: true + darkMode: auto + exportPadding: 20 + order: 50 +layout: + groups: + toolbar: + priority: 35 + direction: row + gap: 0.5rem + byPageType: + "404": + positions: + beforeBody: [] + left: [] + right: [] + content: {} + folder: + exclude: + - reader-mode + positions: + right: [] + tag: + exclude: + - reader-mode + positions: + right: [] + canvas: {} + bases: {} diff --git a/Local/storage/thlab-notes/worker/quartz/components/Body.tsx b/Local/storage/thlab-notes/worker/quartz/components/Body.tsx new file mode 100644 index 0000000..d396f4d --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Body.tsx @@ -0,0 +1,7 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { + return
{children}
+} + +export default (() => Body) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/ConditionalRender.tsx b/Local/storage/thlab-notes/worker/quartz/components/ConditionalRender.tsx new file mode 100644 index 0000000..74a4db0 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/ConditionalRender.tsx @@ -0,0 +1,22 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +type ConditionalRenderConfig = { + component: QuartzComponent + condition: (props: QuartzComponentProps) => boolean +} + +export default ((config: ConditionalRenderConfig) => { + const ConditionalRender: QuartzComponent = (props: QuartzComponentProps) => { + if (config.condition(props)) { + return + } + + return null + } + + ConditionalRender.afterDOMLoaded = config.component.afterDOMLoaded + ConditionalRender.beforeDOMLoaded = config.component.beforeDOMLoaded + ConditionalRender.css = config.component.css + + return ConditionalRender +}) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/Date.tsx b/Local/storage/thlab-notes/worker/quartz/components/Date.tsx new file mode 100644 index 0000000..096cdc6 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Date.tsx @@ -0,0 +1,30 @@ +import { ValidLocale } from "../i18n" +import { QuartzPluginData } from "../plugins/vfile" + +interface Props { + date: Date + locale?: ValidLocale +} + +export type ValidDateType = keyof Required["dates"] + +export function getDate(data: QuartzPluginData): Date | undefined { + if (!data.defaultDateType) { + throw new Error( + `Field 'defaultDateType' was not set. Ensure the CreatedModifiedDate plugin is configured with a 'defaultDateType' option. See https://quartz.jzhao.xyz/plugins/CreatedModifiedDate for more details.`, + ) + } + return data.dates?.[data.defaultDateType] +} + +export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { + return d.toLocaleDateString(locale, { + year: "numeric", + month: "short", + day: "2-digit", + }) +} + +export function Date({ date, locale }: Props) { + return +} diff --git a/Local/storage/thlab-notes/worker/quartz/components/DesktopOnly.tsx b/Local/storage/thlab-notes/worker/quartz/components/DesktopOnly.tsx new file mode 100644 index 0000000..b163eb9 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/DesktopOnly.tsx @@ -0,0 +1,18 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component: QuartzComponent) => { + const Component = component + const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { + return ( +
+ +
+ ) + } + + DesktopOnly.displayName = component.displayName + DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded + DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded + DesktopOnly.css = component?.css + return DesktopOnly +}) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/Flex.tsx b/Local/storage/thlab-notes/worker/quartz/components/Flex.tsx new file mode 100644 index 0000000..70d2149 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Flex.tsx @@ -0,0 +1,59 @@ +import { concatenateResources } from "../util/resources" +import { classNames } from "../util/lang" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +type FlexConfig = { + components: { + Component: QuartzComponent + grow?: boolean + shrink?: boolean + basis?: string + order?: number + align?: "start" | "end" | "center" | "stretch" + justify?: "start" | "end" | "center" | "between" | "around" + }[] + direction?: "row" | "row-reverse" | "column" | "column-reverse" + wrap?: "nowrap" | "wrap" | "wrap-reverse" + gap?: string +} + +export default ((config: FlexConfig) => { + const Flex: QuartzComponent = (props: QuartzComponentProps) => { + const direction = config.direction ?? "row" + const wrap = config.wrap ?? "nowrap" + const gap = config.gap ?? "1rem" + + return ( +
+ {config.components.map((c) => { + const grow = c.grow ? 1 : 0 + const shrink = (c.shrink ?? true) ? 1 : 0 + const basis = c.basis ?? "auto" + const order = c.order ?? 0 + const align = c.align ?? "center" + const justify = c.justify ?? "center" + + return ( +
+ +
+ ) + })} +
+ ) + } + + Flex.afterDOMLoaded = concatenateResources( + ...config.components.map((c) => c.Component.afterDOMLoaded), + ) + Flex.beforeDOMLoaded = concatenateResources( + ...config.components.map((c) => c.Component.beforeDOMLoaded), + ) + Flex.css = concatenateResources(...config.components.map((c) => c.Component.css)) + return Flex +}) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/Head.tsx b/Local/storage/thlab-notes/worker/quartz/components/Head.tsx new file mode 100644 index 0000000..0cb31fa --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Head.tsx @@ -0,0 +1,114 @@ +import { i18n } from "../i18n" +import { FullSlug, getFileExtension, joinSegments, pathToRoot } from "../util/path" +import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources" +import { googleFontHref, googleFontSubsetHref } from "../util/theme" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { unescapeHTML } from "../util/escape" +import { CustomOgImagesEmitterName } from "../../.quartz/plugins" +export default (() => { + const Head: QuartzComponent = ({ + cfg, + fileData, + externalResources, + ctx, + }: QuartzComponentProps) => { + const titleSuffix = cfg.pageTitleSuffix ?? "" + const title = + (fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix + const description = + fileData.frontmatter?.socialDescription ?? + fileData.frontmatter?.description ?? + unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description) + + const { css, js, additionalHead } = externalResources + + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) + const iconPath = joinSegments(baseDir, "static/icon.png") + + // Url of current page + const socialUrl = + fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!) + + const usesCustomOgImage = ctx.cfg.plugins.emitters.some( + (e) => e.name === CustomOgImagesEmitterName, + ) + const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png` + + const coreStylesheet = css[0]?.content + const coreScript = js.find( + (r) => r.loadTime === "beforeDOMReady" && r.contentType === "external", + ) + + return ( + + {title} + + {coreStylesheet && } + {coreScript && coreScript.contentType === "external" && ( + + )} + {cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && ( + <> + + + + {cfg.theme.typography.title && ( + + )} + + )} + + + + + + + + + + + + + {!usesCustomOgImage && ( + <> + + + + + + )} + + {cfg.baseUrl && ( + <> + + + + + )} + + + + + + {css.map((resource) => CSSResourceToStyleElement(resource, true))} + {js + .filter((resource) => resource.loadTime === "beforeDOMReady") + .map((res) => JSResourceToScriptElement(res, true))} + {additionalHead.map((resource) => { + if (typeof resource === "function") { + return resource(fileData) + } else { + return resource + } + })} + + ) + } + + return Head +}) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/Header.tsx b/Local/storage/thlab-notes/worker/quartz/components/Header.tsx new file mode 100644 index 0000000..eba17ae --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Header.tsx @@ -0,0 +1,22 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +const Header: QuartzComponent = ({ children }: QuartzComponentProps) => { + return children.length > 0 ?
{children}
: null +} + +Header.css = ` +header { + display: flex; + flex-direction: row; + align-items: center; + margin: 2rem 0; + gap: 1.5rem; +} + +header h1 { + margin: 0; + flex: auto; +} +` + +export default (() => Header) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/MobileOnly.tsx b/Local/storage/thlab-notes/worker/quartz/components/MobileOnly.tsx new file mode 100644 index 0000000..2c10835 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/MobileOnly.tsx @@ -0,0 +1,18 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component: QuartzComponent) => { + const Component = component + const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { + return ( +
+ +
+ ) + } + + MobileOnly.displayName = component.displayName + MobileOnly.afterDOMLoaded = component?.afterDOMLoaded + MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded + MobileOnly.css = component?.css + return MobileOnly +}) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/PageList.tsx b/Local/storage/thlab-notes/worker/quartz/components/PageList.tsx new file mode 100644 index 0000000..6993b79 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/PageList.tsx @@ -0,0 +1,114 @@ +import { FullSlug, isFolderPath, resolveRelative } from "../util/path" +import { QuartzPluginData } from "../plugins/vfile" +import { Date, getDate } from "./Date" +import { QuartzComponent, QuartzComponentProps } from "./types" + +export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number + +export function byDateAndAlphabetical(): SortFn { + return (f1, f2) => { + // Sort by date/alphabetical + if (f1.dates && f2.dates) { + // sort descending + return getDate(f2)!.getTime() - getDate(f1)!.getTime() + } else if (f1.dates && !f2.dates) { + // prioritize files with dates + return -1 + } else if (!f1.dates && f2.dates) { + return 1 + } + + // otherwise, sort lexographically by title + const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" + const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" + return f1Title.localeCompare(f2Title) + } +} + +export function byDateAndAlphabeticalFolderFirst(): SortFn { + return (f1, f2) => { + // Sort folders first + const f1IsFolder = isFolderPath(f1.slug ?? "") + const f2IsFolder = isFolderPath(f2.slug ?? "") + if (f1IsFolder && !f2IsFolder) return -1 + if (!f1IsFolder && f2IsFolder) return 1 + + // If both are folders or both are files, sort by date/alphabetical + if (f1.dates && f2.dates) { + // sort descending + return getDate(f2)!.getTime() - getDate(f1)!.getTime() + } else if (f1.dates && !f2.dates) { + // prioritize files with dates + return -1 + } else if (!f1.dates && f2.dates) { + return 1 + } + + // otherwise, sort lexographically by title + const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" + const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" + return f1Title.localeCompare(f2Title) + } +} + +type Props = { + limit?: number + sort?: SortFn +} & QuartzComponentProps + +export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => { + const sorter = sort ?? byDateAndAlphabeticalFolderFirst() + let list = allFiles.sort(sorter) + if (limit) { + list = list.slice(0, limit) + } + + return ( +
+ ) +} + +PageList.css = ` +.section h3 { + margin: 0; +} + +.section > .tags { + margin: 0; +} +` diff --git a/Local/storage/thlab-notes/worker/quartz/components/Spacer.tsx b/Local/storage/thlab-notes/worker/quartz/components/Spacer.tsx new file mode 100644 index 0000000..5288752 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/Spacer.tsx @@ -0,0 +1,8 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +function Spacer({ displayClass }: QuartzComponentProps) { + return
+} + +export default (() => Spacer) satisfies QuartzComponentConstructor diff --git a/Local/storage/thlab-notes/worker/quartz/components/external.ts b/Local/storage/thlab-notes/worker/quartz/components/external.ts new file mode 100644 index 0000000..0113445 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/external.ts @@ -0,0 +1,23 @@ +import { componentRegistry } from "./registry" +import { QuartzComponent, QuartzComponentConstructor } from "./types" + +export function External( + name: string, + options?: Options, +): QuartzComponent { + const registered = componentRegistry.get(name) + if (!registered) { + throw new Error( + `External component "${name}" not found. ` + + `Make sure the plugin is installed and components are loaded before layouts are evaluated.`, + ) + } + + const { component } = registered + + if (typeof component === "function") { + return (component as QuartzComponentConstructor)(options as Options) + } + + return component as QuartzComponent +} diff --git a/Local/storage/thlab-notes/worker/quartz/components/frames/DefaultFrame.tsx b/Local/storage/thlab-notes/worker/quartz/components/frames/DefaultFrame.tsx new file mode 100644 index 0000000..d46c120 --- /dev/null +++ b/Local/storage/thlab-notes/worker/quartz/components/frames/DefaultFrame.tsx @@ -0,0 +1,61 @@ +import { PageFrame, PageFrameProps } from "./types" +import HeaderConstructor from "../Header" + +const Header = HeaderConstructor() + +/** + * The default page frame — three-column layout with left sidebar, center + * content (header + body + afterBody), and right sidebar, followed by a footer. + * + * This is the original Quartz layout, extracted from renderPage.tsx. + */ +export const DefaultFrame: PageFrame = { + name: "default", + render({ + componentData, + header, + beforeBody, + pageBody: Content, + afterBody, + left, + right, + footer: Footer, + }: PageFrameProps) { + return ( + <> + +
+ + +
+ +
+ +