Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
097a893c42 | ||
|
|
32c1c25414 | ||
|
|
f01d85c133 | ||
|
|
67b8e64d6a |
No files matched your search
@@ -131,10 +131,10 @@ pnpm -C web build
|
|||||||
|
|
||||||
Gitea 为公开主仓库,GitHub 为单向公开镜像;默认分支均为 main,日常开发使用 dev。
|
Gitea 为公开主仓库,GitHub 为单向公开镜像;默认分支均为 main,日常开发使用 dev。
|
||||||
|
|
||||||
- dev push:容器内检查、Linux amd64 打包与隔离冒烟,通过后发布 `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8位SHA>`,按已验收版本标签部署 DEV,摘要仅用于内部身份校验。日期取提交时间 UTC,完整 SHA 保留在回执;同名异提交拒绝,已验证镜像重试复用。
|
- dev push:容器内检查、Linux amd64 打包与隔离冒烟,通过后发布 `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8位SHA>`,按摘要部署 DEV。日期取提交时间 UTC,完整 SHA 保留在回执;同名异提交拒绝,已验证镜像重试复用。
|
||||||
- main push:独立检查实际合并提交、Linux amd64 构建及容器冒烟,不发布镜像、不部署。仓库内受信 PR 只检查;外部 fork PR 不自动进入共享 Docker runner,不注入发布凭据。
|
- main push:独立检查实际合并提交、Linux amd64 构建及容器冒烟,不发布镜像、不部署。仓库内受信 PR 只检查;外部 fork PR 不自动进入共享 Docker runner,不注入发布凭据。
|
||||||
- DEV 为 `/docker/gitea/dev/RawMark`,容器 `RawMark-dev`,Compose 项目 `rawmark-dev`,监听 `127.0.0.1:21001`,域名 `raw.134333.xyz`,raw-dev 域名停用。DEV 是唯一常驻实例,保存个人真实数据;CI 与冒烟使用隔离合成数据。数据独立;所有部署共用宿主实例 `.deploy.lock`,锁内核对 dev 最新 SHA,pending 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
- DEV 为 `/docker/gitea/dev/RawMark`,容器 `RawMark-dev`,Compose 项目 `rawmark-dev`,监听 `127.0.0.1:21001`,域名 `raw.134333.xyz`,raw-dev 域名停用。DEV 是唯一常驻实例,保存个人真实数据;CI 与冒烟使用隔离合成数据。数据独立;所有部署共用宿主实例 `.deploy.lock`,锁内核对 dev 最新 SHA,pending 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
||||||
- Git 镜像独立于 CI 成败:push/create/delete、每小时对账和手动重试,共用 `/docker/gitea/mirrors/RawMark/.mirror.lock`,仅同步业务分支和 TAG 及完整历史,跟随源端删除。同步代码从 main 读取;`RAWMARK_MIRROR_ENABLED=true` 后启用,凭据统一使用 Gitea 账号级 Secret `ALL_GITHUB_TOKEN`(头像 → 设置 → Actions → 密钥),接入前配置并验证实际权限,仓库级同名项会覆盖账号级值。DEV 镜像不上传 GHCR。main 切换前保持该变量关闭。
|
- Git 镜像独立于 CI 成败:push/create/delete、每小时对账和手动重试,共用 `/docker/gitea/mirrors/RawMark/.mirror.lock`,仅同步业务分支和 TAG 及完整历史,跟随源端删除。同步代码从 main 读取;`RAWMARK_MIRROR_ENABLED=true` 后启用,凭据统一使用 Gitea 账号级 Secret `ALL_GITHUB_TOKEN`(头像 → 设置 → Actions → 密钥),接入前配置并验证实际权限,仓库级同名项会覆盖账号级值。DEV 镜像不上传 GHCR。main 切换前保持该变量关闭。
|
||||||
- CI 凭据 `RAWMARK_GITEA_TOKEN` 需要仓库和 Packages 写权限。runner 保持容量一、`sweep: false`,只运行受信代码;工具链安装在任务容器。资源按任务标签清理,禁止全局 prune。
|
- CI 凭据 `RAWMARK_GITEA_TOKEN` 需要仓库和 Packages 写权限。runner 保持容量一、`sweep: false`,只运行受信代码;工具链安装在任务容器。资源按任务标签清理,禁止全局 prune。
|
||||||
- 开发构建通过 `RAWMARK_BUILD_CHANNEL=development` 派生版本,前端与二进制一致;正式 `VERSION` 不因开发构建改变。开发包为 `RawMark_<版本>_linux_amd64.tar.gz`;历史正式产物不改名。安装包及单次传输保持既有 90,000,000 字节预算。
|
- 开发构建通过 `RAWMARK_BUILD_CHANNEL=development` 派生版本,前端与二进制一致;正式 `VERSION` 不因开发构建改变。开发包为 `RawMark_<版本>_linux_amd64.tar.gz`;历史正式产物不改名。安装包及单次传输保持既有 90,000,000 字节预算。
|
||||||
- 正式发布从受信 main 手动触发,使用日期哈希注解 TAG、六平台程序包与 Linux 双架构镜像;发布 ID 和候选 SHA 固定,发布记录与 .release.lock 位于持久镜像目录。GitHub 引用同步复用镜像锁,Pages Demo 不切换。双端发布和 GHCR 公开拉取验收后结束发布并标记预约完成,不部署 MAIN、不更新 DEV。DEV 启停入口为实例内 `start.sh` / `stop.sh`,由 `deploy/` 随 CD 安装和更新(755);与 CD 共用实例锁,pending 阻止启动。实例环境文件优先,拒绝覆盖本地改过的脚本。
|
- 正式发布从受信 main 手动触发,使用日期哈希注解 TAG、六平台程序包与 Linux 双架构镜像;发布 ID 和候选 SHA 固定,发布记录与 .release.lock 位于持久镜像目录。GitHub 引用同步复用镜像锁,Pages Demo 不切换。双端发布和 GHCR 公开拉取验收后结束发布并标记预约完成,不部署 MAIN、不更新 DEV。DEV 批量管理入口为 `/docker/gitea/dev/manage.sh start|stop`,与 CD 共用实例锁,pending 阻止启动。
|
||||||
@@ -3,7 +3,7 @@ name: ${RAWMARK_PROJECT:?Set RAWMARK_PROJECT}
|
|||||||
services:
|
services:
|
||||||
rawmark:
|
rawmark:
|
||||||
container_name: ${RAWMARK_CONTAINER_NAME:?Set RAWMARK_CONTAINER_NAME}
|
container_name: ${RAWMARK_CONTAINER_NAME:?Set RAWMARK_CONTAINER_NAME}
|
||||||
image: ${RAWMARK_IMAGE:?Set a verified registry image tag}
|
image: ${RAWMARK_IMAGE:?Set a verified registry image digest}
|
||||||
ports:
|
ports:
|
||||||
- "${RAWMARK_BIND:-127.0.0.1}:${RAWMARK_PORT:?Set RAWMARK_PORT}:8080"
|
- "${RAWMARK_BIND:-127.0.0.1}:${RAWMARK_PORT:?Set RAWMARK_PORT}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Manage only this DEV instance under the deployment lock.
|
|
||||||
set -euo pipefail
|
|
||||||
if (( $# != 0 )); then
|
|
||||||
printf '用法:bash %s\n' "$0" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
instance_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
||||||
cd -- "$instance_dir"
|
|
||||||
for tool in docker flock env; do
|
|
||||||
command -v "$tool" >/dev/null 2>&1 || { printf '缺少命令:%s\n' "$tool" >&2; exit 1; }
|
|
||||||
done
|
|
||||||
exec 9>>.deploy.lock
|
|
||||||
flock -n 9 || { printf '实例正在部署或启停,请稍后重试\n' >&2; exit 1; }
|
|
||||||
files=()
|
|
||||||
for filename in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
|
||||||
[[ ! -f $filename ]] || files+=("$filename")
|
|
||||||
done
|
|
||||||
if (( ${#files[@]} != 1 )); then
|
|
||||||
printf '实例必须有且只有一个 Compose 主文件,当前找到 %d 个\n' "${#files[@]}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Keep Docker client access; resolve application settings only from instance files.
|
|
||||||
compose=(env -i "PATH=$PATH")
|
|
||||||
for variable in HOME DOCKER_CONFIG DOCKER_HOST DOCKER_CONTEXT DOCKER_TLS_VERIFY DOCKER_CERT_PATH XDG_RUNTIME_DIR SSH_AUTH_SOCK; do
|
|
||||||
if [[ -v $variable ]]; then
|
|
||||||
compose+=("$variable=${!variable}")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
compose+=(docker compose --project-directory "$instance_dir" -f "$instance_dir/${files[0]}")
|
|
||||||
# Explicit env files disable Compose's implicit .env, so include both in order.
|
|
||||||
for filename in .env images.env; do
|
|
||||||
if [[ -f $filename ]]; then
|
|
||||||
compose+=(--env-file "$instance_dir/$filename")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ -e deployment.pending.json ]]; then
|
|
||||||
printf '实例存在未完成部署,请先检查并恢复部署状态\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
"${compose[@]}" config --quiet
|
|
||||||
# Reuse the verified local image without building or pulling.
|
|
||||||
"${compose[@]}" up -d --no-build --pull never
|
|
||||||
printf '已启动实例:%s\n' "${instance_dir##*/}"
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Manage only this DEV instance under the deployment lock.
|
|
||||||
set -euo pipefail
|
|
||||||
if (( $# != 0 )); then
|
|
||||||
printf '用法:bash %s\n' "$0" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
instance_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
||||||
cd -- "$instance_dir"
|
|
||||||
for tool in docker flock env; do
|
|
||||||
command -v "$tool" >/dev/null 2>&1 || { printf '缺少命令:%s\n' "$tool" >&2; exit 1; }
|
|
||||||
done
|
|
||||||
exec 9>>.deploy.lock
|
|
||||||
flock -n 9 || { printf '实例正在部署或启停,请稍后重试\n' >&2; exit 1; }
|
|
||||||
files=()
|
|
||||||
for filename in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
|
||||||
[[ ! -f $filename ]] || files+=("$filename")
|
|
||||||
done
|
|
||||||
if (( ${#files[@]} != 1 )); then
|
|
||||||
printf '实例必须有且只有一个 Compose 主文件,当前找到 %d 个\n' "${#files[@]}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Keep Docker client access; resolve application settings only from instance files.
|
|
||||||
compose=(env -i "PATH=$PATH")
|
|
||||||
for variable in HOME DOCKER_CONFIG DOCKER_HOST DOCKER_CONTEXT DOCKER_TLS_VERIFY DOCKER_CERT_PATH XDG_RUNTIME_DIR SSH_AUTH_SOCK; do
|
|
||||||
if [[ -v $variable ]]; then
|
|
||||||
compose+=("$variable=${!variable}")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
compose+=(docker compose --project-directory "$instance_dir" -f "$instance_dir/${files[0]}")
|
|
||||||
# Explicit env files disable Compose's implicit .env, so include both in order.
|
|
||||||
for filename in .env images.env; do
|
|
||||||
if [[ -f $filename ]]; then
|
|
||||||
compose+=(--env-file "$instance_dir/$filename")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
"${compose[@]}" config --quiet
|
|
||||||
# Preserve containers, networks, volumes and persistent data.
|
|
||||||
"${compose[@]}" stop
|
|
||||||
printf '已停止实例:%s\n' "${instance_dir##*/}"
|
|
||||||
@@ -53,12 +53,6 @@ Send `Accept-Language: en` or `Accept-Language: zh-CN` to select the language of
|
|||||||
|
|
||||||
Account language is the `locale` field on `/settings`, supporting only `en` and `zh-CN`. PATCH with `null` removes the override and restores default English. The field is exported, imported, and synchronized with other settings. The sign-in page's temporary choice does not write to the account. Request headers affect only the current response, not account language.
|
Account language is the `locale` field on `/settings`, supporting only `en` and `zh-CN`. PATCH with `null` removes the override and restores default English. The field is exported, imported, and synchronized with other settings. The sign-in page's temporary choice does not write to the account. Request headers affect only the current response, not account language.
|
||||||
|
|
||||||
## Pinned tabs
|
|
||||||
|
|
||||||
The `/settings` field `pinnedTabs` is an array of Markdown paths relative to the vault. The browser saves pin/unpin actions to the server and restores existing pinned notes on other devices, including after an event connection reconnects. An explicit empty array means no pinned tabs; if the field is absent, the browser migrates its legacy local pins once. Resetting settings stores an empty array so older device caches cannot restore cleared pins. Concurrent replacements of this list use the last server write.
|
|
||||||
|
|
||||||
Unpinned tabs and the active tab remain local to each device. Remote unpinning keeps local tabs and unsaved edits open. Explicitly closing a pinned tab also removes its server pin. Renaming notes or folders through the workspace updates pinned paths; unavailable paths are skipped when opening tabs.
|
|
||||||
|
|
||||||
## Browser sign-in
|
## Browser sign-in
|
||||||
|
|
||||||
There are no usernames, registration, or roles. Before initialization, `POST /auth/setup` sets the password using `{ password }` and returns a session. An initialized instance cannot be initialized again.
|
There are no usernames, registration, or roles. Before initialization, `POST /auth/setup` sets the password using `{ password }` and returns a session. An initialized instance cannot be initialized again.
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ curl --fail-with-body \
|
|||||||
|
|
||||||
账户语言位于 `/settings` 的 `locale` 字段,仅支持 `en`、`zh-CN`;PATCH 传 `null` 删除覆盖,恢复默认英文。该字段与其他设置一同导出、导入和同步;登录页的临时选择不写入账户。请求头只影响当前响应,不修改账户语言。
|
账户语言位于 `/settings` 的 `locale` 字段,仅支持 `en`、`zh-CN`;PATCH 传 `null` 删除覆盖,恢复默认英文。该字段与其他设置一同导出、导入和同步;登录页的临时选择不写入账户。请求头只影响当前响应,不修改账户语言。
|
||||||
|
|
||||||
## 固定标签
|
|
||||||
|
|
||||||
`/settings` 的 `pinnedTabs` 字段保存相对笔记库的 Markdown 路径数组。浏览器将固定和取消固定写入服务器,其他设备打开时及事件连接重连后会恢复仍存在的固定笔记。显式空数组表示没有固定标签;字段尚不存在时,浏览器会将旧的本地固定标签迁移一次。重置设置会保存空数组,避免其他设备的旧缓存恢复已清空的固定记录。多个设备同时替换该列表时,以服务器最后一次写入为准。
|
|
||||||
|
|
||||||
未固定标签和当前选中的标签仍属于各设备本地状态。远程取消固定不会关闭本机标签或丢弃未保存内容;主动关闭固定标签也会移除服务器上的固定记录。通过工作区重命名笔记或目录时会更新固定路径,打开标签时跳过不存在的路径。
|
|
||||||
|
|
||||||
## 浏览器登录
|
## 浏览器登录
|
||||||
|
|
||||||
实例没有用户名、注册或角色系统。尚未初始化时,`POST /auth/setup` 使用 `{ password }` 设置密码并返回会话;已经初始化的实例不能再次初始化。
|
实例没有用户名、注册或角色系统。尚未初始化时,`POST /auth/setup` 使用 `{ password }` 设置密码并返回会话;已经初始化的实例不能再次初始化。
|
||||||
|
|||||||
+3
-12
@@ -146,7 +146,7 @@ In addition to application backups, stop the service before upgrades and back up
|
|||||||
|
|
||||||
1. Check the target version's behavior and data compatibility; retain the current image and deployment configuration.
|
1. Check the target version's behavior and data compatibility; retain the current image and deployment configuration.
|
||||||
2. Stop the old service and back up application data and any external vault.
|
2. Stop the old service and back up application data and any external vault.
|
||||||
3. Pull the selected prebuilt image version tag and start with the same mounts. Source builds are optional contributor workflows, not managed production updates. Do not run old and new services concurrently.
|
3. Pull the selected prebuilt image digest and start with the same mounts. Source builds are optional contributor workflows, not managed production updates. Do not run old and new services concurrently.
|
||||||
4. Verify container health, sign-in, notes, and attachments. Roll back using the matching previous image and data backup.
|
4. Verify container health, sign-in, notes, and attachments. Roll back using the matching previous image and data backup.
|
||||||
|
|
||||||
The Bearer authentication upgrade requires the frontend and backend to be released together. Old Cookie-based pages require signing in again. Startup creates the resource-ticket table and indexes through the existing SQLite initialization flow; notes, passwords, settings, and existing API tokens are retained. Third-party clients must follow the [current JSON protocol](api.md).
|
The Bearer authentication upgrade requires the frontend and backend to be released together. Old Cookie-based pages require signing in again. Startup creates the resource-ticket table and indexes through the existing SQLite initialization flow; notes, passwords, settings, and existing API tokens are retained. Third-party clients must follow the [current JSON protocol](api.md).
|
||||||
@@ -175,19 +175,10 @@ RAWMARK_CONTAINER_NAME=RawMark-dev
|
|||||||
RAWMARK_BIND=127.0.0.1
|
RAWMARK_BIND=127.0.0.1
|
||||||
RAWMARK_PORT=21001
|
RAWMARK_PORT=21001
|
||||||
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||||
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark:<verified-version>
|
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark@sha256:<verified-digest>
|
||||||
```
|
```
|
||||||
|
|
||||||
Dev pushes deploy verified Gitea development version tags after checks and isolated smoke tests. Main pushes and PRs only validate. Manual formal releases publish packages and Gitea/GHCR images with GitHub mirrors; they do not create MAIN or update DEV. The instance-local controls share `.deploy.lock` with CD. Startup rejects unresolved pending deployments; stopping remains available after acquiring the lock.
|
Dev pushes deploy verified Gitea development digests after checks and isolated smoke tests. Main pushes and PRs only validate. Manual formal releases publish packages and Gitea/GHCR images with GitHub mirrors; they do not create MAIN or update DEV. `/docker/gitea/dev/manage.sh start|stop` manages first-level projects using instance `.deploy.lock` files and rejects starts with unresolved pending deployments.
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /docker/gitea/dev/RawMark/start.sh
|
|
||||||
bash /docker/gitea/dev/RawMark/stop.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
CD installs `deploy/start.sh` and `deploy/stop.sh` into the instance with mode 755 before stopping the service, on both first deployment and updates. The `.control-scripts.json` receipt tracks installed hashes; local edits or unrecognized existing scripts require review before deployment and are never silently overwritten. Initial manual setup can copy these two files after checking any existing entries.
|
|
||||||
|
|
||||||
Controls explicitly load `.env`, followed by `images.env` when present. Application and Compose variables from the caller cannot override those files; only PATH, HOME and the listed Docker client connection/configuration variables are retained. RawMark CD stores its verified image in `.env`; no separate `images.env` is needed. Startup reuses local images with `--no-build --pull never`; stopping preserves containers, networks, volumes and data. A successful start command means Compose accepted startup; use container health to confirm readiness.
|
|
||||||
|
|
||||||
Deployment stops the service and backs up complete data, including SQLite WAL, configuration and previous-image details before writing `deployment.pending.json`. Successful health checks write `deployment.json` and clear pending. A failed candidate is stopped while its backup and pending record remain; no automatic data restore or program downgrade occurs. Recovery and private-data migration require explicit authorization and compatibility review. Never run two instances sharing writable data.
|
Deployment stops the service and backs up complete data, including SQLite WAL, configuration and previous-image details before writing `deployment.pending.json`. Successful health checks write `deployment.json` and clear pending. A failed candidate is stopped while its backup and pending record remain; no automatic data restore or program downgrade occurs. Recovery and private-data migration require explicit authorization and compatibility review. Never run two instances sharing writable data.
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ docker run -d --name rawmark --restart unless-stopped \
|
|||||||
|
|
||||||
1. 确认目标版本的行为和数据兼容性,保留当前镜像及部署配置。
|
1. 确认目标版本的行为和数据兼容性,保留当前镜像及部署配置。
|
||||||
2. 停止旧服务,备份应用数据与外置笔记库。
|
2. 停止旧服务,备份应用数据与外置笔记库。
|
||||||
3. 拉取选定的预构建镜像版本标签,使用同一份挂载启动新实例;源码构建仅供贡献者选择,不用于托管生产更新。不要并行运行新旧服务。
|
3. 拉取选定的预构建镜像摘要,使用同一份挂载启动新实例;源码构建仅供贡献者选择,不用于托管生产更新。不要并行运行新旧服务。
|
||||||
4. 核对容器健康、登录、笔记与附件。回退时使用配套的旧镜像与数据备份。
|
4. 核对容器健康、登录、笔记与附件。回退时使用配套的旧镜像与数据备份。
|
||||||
|
|
||||||
Bearer 认证升级需要前后端一起发布。旧 Cookie 页面不能继续使用,需重新登录;启动程序会沿现有 SQLite 初始化流程创建资源票据表和索引,笔记、密码、设置与已有 API 令牌保留。第三方客户端的 JSON 解包方式须遵循[当前协议](api.zh-CN.md)。
|
Bearer 认证升级需要前后端一起发布。旧 Cookie 页面不能继续使用,需重新登录;启动程序会沿现有 SQLite 初始化流程创建资源票据表和索引,笔记、密码、设置与已有 API 令牌保留。第三方客户端的 JSON 解包方式须遵循[当前协议](api.zh-CN.md)。
|
||||||
@@ -175,19 +175,10 @@ RAWMARK_CONTAINER_NAME=RawMark-dev
|
|||||||
RAWMARK_BIND=127.0.0.1
|
RAWMARK_BIND=127.0.0.1
|
||||||
RAWMARK_PORT=21001
|
RAWMARK_PORT=21001
|
||||||
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||||
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark:<已验证版本>
|
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark@sha256:<已验证摘要>
|
||||||
```
|
```
|
||||||
|
|
||||||
DEV push 检查及隔离验收通过后自动部署 Gitea 开发镜像;main push 与 PR 只检查。正式 Release 手动发布程序包、Gitea/GHCR 镜像及 GitHub 镜像,不创建 MAIN 或更新 DEV。实例启停脚本与 CD 共用 `.deploy.lock`。pending 未处理时拒绝启动,停止仍可在取得锁后执行。
|
DEV push 检查及隔离验收通过后自动部署 Gitea 开发镜像;main push 与 PR 只检查。正式 Release 手动发布程序包、Gitea/GHCR 镜像及 GitHub 镜像,不创建 MAIN 或更新 DEV。`/docker/gitea/dev/manage.sh start|stop` 批量管理一级项目,共用实例 `.deploy.lock`,pending 未处理时拒绝启动。
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /docker/gitea/dev/RawMark/start.sh
|
|
||||||
bash /docker/gitea/dev/RawMark/stop.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
CD 在首次及后续部署停服前,将 `deploy/start.sh`、`deploy/stop.sh` 安装到实例并设置权限 755。`.control-scripts.json` 记录已安装脚本的哈希;遇到本地修改或无法识别的已有脚本会要求先审查,不会静默覆盖。首次手动配置也可在检查已有入口后复制这两个文件。
|
|
||||||
|
|
||||||
脚本显式依次加载 `.env` 和存在的 `images.env`,调用者的应用及 Compose 环境变量不能覆盖实例配置;只保留 PATH、HOME 及脚本列明的 Docker 客户端连接与配置变量。RawMark CD 将已验证镜像写入 `.env`,无需另建 `images.env`。启动通过 `--no-build --pull never` 复用本地镜像,停止保留容器、网络、卷和数据。启动命令成功表示 Compose 已接受启动,服务就绪仍需核对容器健康。
|
|
||||||
|
|
||||||
部署停止服务后备份完整数据(含 SQLite WAL)、配置和旧镜像信息,再写入 `deployment.pending.json`;健康检查通过才写入 `deployment.json` 并清除 pending。候选失败时停止候选、保留备份,不自动降级程序或恢复数据库。恢复和私人数据迁移需明确授权及兼容性核查;不得同时运行共享可写数据的新旧实例。
|
部署停止服务后备份完整数据(含 SQLite WAL)、配置和旧镜像信息,再写入 `deployment.pending.json`;健康检查通过才写入 `deployment.json` 并清除 pending。候选失败时停止候选、保留备份,不自动降级程序或恢复数据库。恢复和私人数据迁移需明确授权及兼容性核查;不得同时运行共享可写数据的新旧实例。
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -90,7 +90,7 @@ Default font resources are checked in; ordinary builds require no download or co
|
|||||||
python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip
|
python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
The script generates WOFF2 files and `maple-font.css` while retaining the upstream license; `--css-only` regenerates declarations without rebuilding font binaries. Fonts use swap rendering, so downloads never hide the page. After first paint, regular Maple chunks (about 6.72 MB) load in batches of two; other weights load on demand. PWA installation skips font files and caches public build fonts after use. Code blocks use highlight.js with fixed Atom One Dark; theme changes must not reintroduce previous highlighters or color schemes.
|
The script generates WOFF2 files and `maple-font.css` while retaining the upstream license. Code blocks use highlight.js with fixed Atom One Dark; theme changes must not reintroduce previous highlighters or color schemes.
|
||||||
|
|
||||||
Plugin SDK types are in [common/plugin-api/index.d.ts](../web/src/common/plugin-api/index.d.ts); loading and lifecycle code lives in `common/plugins/`. Plugins run only on the frontend, and installers must trust their code. Namespaces, disabling, and resource versioning do not constitute a permission sandbox. Editor-extension failures preserve document contents and disable the relevant extensions on the current page.
|
Plugin SDK types are in [common/plugin-api/index.d.ts](../web/src/common/plugin-api/index.d.ts); loading and lifecycle code lives in `common/plugins/`. Plugins run only on the frontend, and installers must trust their code. Namespaces, disabling, and resource versioning do not constitute a permission sandbox. Editor-extension failures preserve document contents and disable the relevant extensions on the current page.
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ Backend `internal/i18n/` preserves message IDs and parameters. Errors render at
|
|||||||
|
|
||||||
Gitea is the public source repository; GitHub is a strict one-way Git mirror. Both use main as the default branch and dev for daily development.
|
Gitea is the public source repository; GitHub is a strict one-way Git mirror. Both use main as the default branch and dev for daily development.
|
||||||
|
|
||||||
- A dev push checks, packages and smoke-tests Linux amd64 in CI containers, publishes only `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8-char-SHA>`, then deploys its verified version tag. The date comes from the commit timestamp in UTC. Receipts retain the full SHA; conflicting versions fail and verified images are reused on retry.
|
- A dev push checks, packages and smoke-tests Linux amd64 in CI containers, publishes only `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8-char-SHA>`, then deploys its verified digest. The date comes from the commit timestamp in UTC. Receipts retain the full SHA; conflicting versions fail and verified images are reused on retry.
|
||||||
- A main push independently checks and smoke-tests the actual merged commit without publishing images or deploying. Trusted same-repository PRs only check; external fork PRs do not automatically enter the shared Docker runner or receive publication credentials.
|
- A main push independently checks and smoke-tests the actual merged commit without publishing images or deploying. Trusted same-repository PRs only check; external fork PRs do not automatically enter the shared Docker runner or receive publication credentials.
|
||||||
- DEV lives at `/docker/gitea/dev/RawMark`, container `RawMark-dev`, explicit Compose project `rawmark-dev`, loopback port 21001 and domain `raw.134333.xyz`; raw-dev is retired. DEV is the only persistent instance and holds private user data; CI smoke tests use isolated synthetic data. All deployment entrypoints share the instance `.deploy.lock`; stale dev commits are skipped and unresolved pending deployments block updates. Existing data is backed up even when no old container exists.
|
- DEV lives at `/docker/gitea/dev/RawMark`, container `RawMark-dev`, explicit Compose project `rawmark-dev`, loopback port 21001 and domain `raw.134333.xyz`; raw-dev is retired. DEV is the only persistent instance and holds private user data; CI smoke tests use isolated synthetic data. All deployment entrypoints share the instance `.deploy.lock`; stale dev commits are skipped and unresolved pending deployments block updates. Existing data is backed up even when no old container exists.
|
||||||
- Git mirroring runs independently on push/create/delete, hourly reconciliation and manual retry, sharing `/docker/gitea/mirrors/RawMark/.mirror.lock`. Only business branches, tags and complete history are mirrored, including deletions. Control code comes from main. Enable `RAWMARK_MIRROR_ENABLED=true` after the main cutover and configure the account-level `ALL_GITHUB_TOKEN` in Gitea user Settings → Actions → Secrets with repository/workflow write access. DEV images never go to GHCR.
|
- Git mirroring runs independently on push/create/delete, hourly reconciliation and manual retry, sharing `/docker/gitea/mirrors/RawMark/.mirror.lock`. Only business branches, tags and complete history are mirrored, including deletions. Control code comes from main. Enable `RAWMARK_MIRROR_ENABLED=true` after the main cutover and configure the account-level `ALL_GITHUB_TOKEN` in Gitea user Settings → Actions → Secrets with repository/workflow write access. DEV images never go to GHCR.
|
||||||
- `RAWMARK_GITEA_TOKEN` requires repository and Packages write access. Keep runner capacity one and sweep disabled; only trusted code runs on this Docker-socket runner. Install toolchains inside CI containers and clean only resources labeled for the current attempt.
|
- `RAWMARK_GITEA_TOKEN` requires repository and Packages write access. Keep runner capacity one and sweep disabled; only trusted code runs on this Docker-socket runner. Install toolchains inside CI containers and clean only resources labeled for the current attempt.
|
||||||
- `RAWMARK_BUILD_CHANNEL=development` derives a consistent frontend/binary version without editing the formal VERSION file. Development archives use `RawMark_<version>_linux_amd64.tar.gz`; historical release artifacts retain their names. Keep the existing 90,000,000-byte transfer budget.
|
- `RAWMARK_BUILD_CHANNEL=development` derives a consistent frontend/binary version without editing the formal VERSION file. Development archives use `RawMark_<version>_linux_amd64.tar.gz`; historical release artifacts retain their names. Keep the existing 90,000,000-byte transfer budget.
|
||||||
- Formal releases run manually from trusted main, require successful main-push CI, and publish six native packages plus Linux amd64/arm64 images with date/hash versions. Fixed release IDs retain resumable checkpoints under the persistent release lock. After both releases and public GHCR digest pulls pass, the reservation is marked complete without deploying or restarting any instance. Use `bash /docker/gitea/dev/RawMark/start.sh` or `bash /docker/gitea/dev/RawMark/stop.sh` to control only this instance, with the deployment lock and pending-start protection. CD installs and updates these executable scripts from `deploy/`, preserving locally modified entries for review. Keep Pages unchanged.
|
- Formal releases run manually from trusted main, require successful main-push CI, and publish six native packages plus Linux amd64/arm64 images with date/hash versions. Fixed release IDs retain resumable checkpoints under the persistent release lock. After both releases and public GHCR digest pulls pass, the reservation is marked complete without deploying or restarting any instance. Use `/docker/gitea/dev/manage.sh start|stop` for bulk DEV management with instance locks and pending-start protection. Keep Pages unchanged.
|
||||||
@@ -404,26 +404,6 @@ func TestSettingsMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPinnedTabsSharedAcrossSessions(t *testing.T) {
|
|
||||||
f := newFixture(t)
|
|
||||||
other := newClient()
|
|
||||||
f.login(other, "password123")
|
|
||||||
f.do(f.admin, "PATCH", "/api/v1/settings", `{"pinnedTabs":["a.md","目录/b.md"]}`, nil).Body.Close()
|
|
||||||
got := decode[struct{ PinnedTabs []string }](t, f.do(other, "GET", "/api/v1/settings", "", nil))
|
|
||||||
if len(got.PinnedTabs) != 2 || got.PinnedTabs[0] != "a.md" || got.PinnedTabs[1] != "目录/b.md" {
|
|
||||||
t.Fatalf("另一会话未读取到固定标签: %v", got.PinnedTabs)
|
|
||||||
}
|
|
||||||
f.do(other, "PATCH", "/api/v1/settings", `{"pinnedTabs":[]}`, nil).Body.Close()
|
|
||||||
again := decode[struct{ PinnedTabs []string }](t, f.do(f.admin, "GET", "/api/v1/settings", "", nil))
|
|
||||||
if again.PinnedTabs == nil || len(again.PinnedTabs) != 0 {
|
|
||||||
t.Fatalf("取消固定必须保存为空列表: %v", again.PinnedTabs)
|
|
||||||
}
|
|
||||||
stored, ok, err := f.store.Setting(settingsKey)
|
|
||||||
if err != nil || !ok || !strings.Contains(stored, `"pinnedTabs":[]`) {
|
|
||||||
t.Fatalf("固定标签未持久化到数据库: %q, %v", stored, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangePassword(t *testing.T) {
|
func TestChangePassword(t *testing.T) {
|
||||||
f := newFixture(t)
|
f := newFixture(t)
|
||||||
other := newClient()
|
other := newClient()
|
||||||
|
|||||||
@@ -156,19 +156,17 @@ func TestSettingsPackageRoundTripMergesResourcesAndPreservesAccount(t *testing.T
|
|||||||
func TestResetPreferencesPreservesPluginDataAndCredentials(t *testing.T) {
|
func TestResetPreferencesPreservesPluginDataAndCredentials(t *testing.T) {
|
||||||
f := newFixture(t)
|
f := newFixture(t)
|
||||||
f.write("note.md", "source")
|
f.write("note.md", "source")
|
||||||
f.store.SetSetting(settingsKey, `{"fontSize":24,"trashRetentionDays":0,"pinnedTabs":["note.md"]}`)
|
f.store.SetSetting(settingsKey, `{"fontSize":24,"trashRetentionDays":0}`)
|
||||||
f.store.SetSetting(pluginEnabledKey, `{"example":true}`)
|
f.store.SetSetting(pluginEnabledKey, `{"example":true}`)
|
||||||
f.store.SetSetting("plugin.data.example", `{"important":"content"}`)
|
f.store.SetSetting("plugin.data.example", `{"important":"content"}`)
|
||||||
if status := f.status(f.admin, "POST", "/api/v1/maintenance/settings/reset", `{"confirmed":false}`, nil); status != 400 {
|
if status := f.status(f.admin, "POST", "/api/v1/maintenance/settings/reset", `{"confirmed":false}`, nil); status != 400 {
|
||||||
t.Fatal(status)
|
t.Fatal(status)
|
||||||
}
|
}
|
||||||
assertOK(t, f.do(f.admin, "POST", "/api/v1/maintenance/settings/reset", `{"confirmed":true}`, nil))
|
assertOK(t, f.do(f.admin, "POST", "/api/v1/maintenance/settings/reset", `{"confirmed":true}`, nil))
|
||||||
if _, ok, err := f.store.Setting(pluginEnabledKey); err != nil || ok {
|
for _, key := range []string{settingsKey, pluginEnabledKey} {
|
||||||
t.Fatal("插件启用状态未重置")
|
if _, ok, err := f.store.Setting(key); err != nil || ok {
|
||||||
|
t.Fatalf("未重置 %s", key)
|
||||||
}
|
}
|
||||||
settings := decode[map[string]json.RawMessage](t, f.do(f.admin, "GET", "/api/v1/settings", "", nil))
|
|
||||||
if len(settings) != 1 || string(settings["pinnedTabs"]) != "[]" {
|
|
||||||
t.Fatalf("重置必须清空固定标签并阻止旧设备再次迁移: %v", settings)
|
|
||||||
}
|
}
|
||||||
if value, ok, err := f.store.Setting("plugin.data.example"); err != nil || !ok || value != `{"important":"content"}` {
|
if value, ok, err := f.store.Setting("plugin.data.example"); err != nil || !ok || value != `{"important":"content"}` {
|
||||||
t.Fatal("插件数据丢失")
|
t.Fatal("插件数据丢失")
|
||||||
|
|||||||
@@ -481,7 +481,7 @@
|
|||||||
"bearer": []
|
"bearer": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Merge top-level keys: supplied values replace existing values, and null removes a key. On success, broadcast a settings event over WebSocket. pinnedTabs stores vault-relative Markdown paths shared across devices; an empty array explicitly clears pins, while an absent key allows the browser to migrate legacy local pins. Concurrent replacements use the last server write. trashRetentionDays is a nonnegative safe integer (default 30; 0 disables automatic deletion); null restores the default. Retention applies to existing and new recycle-bin entries. Startup and hourly cleanup permanently delete expired entries based on their original deletion times.",
|
"description": "Merge top-level keys: supplied values replace existing values, and null removes a key. On success, broadcast a settings event over WebSocket. trashRetentionDays is a nonnegative safe integer (default 30; 0 disables automatic deletion); null restores the default. Retention applies to existing and new recycle-bin entries. Startup and hourly cleanup permanently delete expired entries based on their original deletion times.",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
@@ -490,16 +490,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"properties": {
|
"properties": {
|
||||||
"pinnedTabs": {
|
|
||||||
"type": [
|
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Ordered vault-relative Markdown paths. Empty means no pins; null removes the setting."
|
|
||||||
},
|
|
||||||
"trashRetentionDays": {
|
"trashRetentionDays": {
|
||||||
"type": [
|
"type": [
|
||||||
"integer",
|
"integer",
|
||||||
@@ -3799,7 +3789,6 @@
|
|||||||
"/maintenance/settings/reset": {
|
"/maintenance/settings/reset": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Reset confirmed preferences while retaining plugin storage",
|
"summary": "Reset confirmed preferences while retaining plugin storage",
|
||||||
"description": "Reset application preferences and plugin activation while preserving plugin data and credentials. Store pinnedTabs as an explicit empty array so legacy browser caches cannot migrate cleared pins back to the server.",
|
|
||||||
"tags": [
|
"tags": [
|
||||||
"Maintenance"
|
"Maintenance"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -56,19 +56,8 @@ func (s *Store) ReplacePreferences(values map[string]json.RawMessage) error {
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
func (s *Store) ResetPreferences() error {
|
func (s *Store) ResetPreferences() error {
|
||||||
tx, err := s.db.Begin()
|
_, err := s.db.Exec(`DELETE FROM settings WHERE key IN ('app.settings','plugins.enabled')`)
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
if _, err = tx.Exec(`DELETE FROM settings WHERE key IN ('app.settings','plugins.enabled')`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// An explicit empty list prevents older devices from migrating their local pins after a reset.
|
|
||||||
if _, err = tx.Exec(`INSERT INTO settings(key,value) VALUES ('app.settings',?)`, `{"pinnedTabs":[]}`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
}
|
||||||
func (s *Store) ClearSessions() error { _, err := s.db.Exec(`DELETE FROM sessions`); return err }
|
func (s *Store) ClearSessions() error { _, err := s.db.Exec(`DELETE FROM sessions`); return err }
|
||||||
|
|
||||||
|
|||||||
@@ -48,31 +48,3 @@ func TestOpenExistingUsesNativeEscapedPathWithoutCreating(t *testing.T) {
|
|||||||
t.Fatalf("missing database was created: %v", err)
|
t.Fatalf("missing database was created: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetPreferencesRollsBackIfPinResetFails(t *testing.T) {
|
|
||||||
st, err := Open(filepath.Join(t.TempDir(), "rawmark.db"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer st.Close()
|
|
||||||
original := map[string]string{
|
|
||||||
"app.settings": `{"fontSize":24,"pinnedTabs":["note.md"]}`,
|
|
||||||
"plugins.enabled": `{"example":true}`,
|
|
||||||
}
|
|
||||||
for key, value := range original {
|
|
||||||
if err := st.SetSetting(key, value); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := st.db.Exec(`CREATE TRIGGER reject_pin_reset BEFORE INSERT ON settings WHEN NEW.key='app.settings' BEGIN SELECT RAISE(ABORT,'intentional reset failure'); END`); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := st.ResetPreferences(); err == nil {
|
|
||||||
t.Fatal("固定标签重置失败必须返回错误")
|
|
||||||
}
|
|
||||||
for key, value := range original {
|
|
||||||
if got, ok, err := st.Setting(key); err != nil || !ok || got != value {
|
|
||||||
t.Fatalf("重置失败未回滚 %s: %q, %v", key, got, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-27
@@ -4,7 +4,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import contextlib
|
import contextlib
|
||||||
import fcntl
|
import fcntl
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -47,35 +46,12 @@ def save(path, data):
|
|||||||
temporary.unlink(missing_ok=True)
|
temporary.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def install_controls(directory):
|
|
||||||
# Recognize the original templates; subsequent updates verify the installed receipt.
|
|
||||||
legacy = {'start.sh': 'b4709f36a66b428671a09f373f640a99259845924779dcb122f5cd9c17e85ab6',
|
|
||||||
'stop.sh': '83348a7781b7d738a715bde51387951041a5cfcd31796805422ef83139a21571'}
|
|
||||||
receipt = directory / '.control-scripts.json'
|
|
||||||
previous = json.loads(receipt.read_text()) if receipt.exists() else {}
|
|
||||||
sources = Path(__file__).resolve().parents[1] / 'deploy'
|
|
||||||
scripts = {name: (sources / name).read_bytes() for name in legacy}
|
|
||||||
hashes = {name: hashlib.sha256(data).hexdigest() for name, data in scripts.items()}
|
|
||||||
# Check both entries before replacing either; preserve custom or locally edited scripts.
|
|
||||||
for name in scripts:
|
|
||||||
target = directory / name
|
|
||||||
require(not target.is_symlink(), f'Refusing to replace symlink: {target}')
|
|
||||||
if target.exists():
|
|
||||||
current = hashlib.sha256(target.read_bytes()).hexdigest()
|
|
||||||
require(current in (legacy[name], hashes[name], previous.get(name)),
|
|
||||||
f'Locally modified control script requires review: {target}')
|
|
||||||
for name, data in scripts.items():
|
|
||||||
save(directory / name, data)
|
|
||||||
(directory / name).chmod(0o755)
|
|
||||||
save(receipt, (json.dumps(hashes, indent=2) + '\n').encode())
|
|
||||||
|
|
||||||
|
|
||||||
def deployment(directory, channel, image, commit, version, client_container=None):
|
def deployment(directory, channel, image, commit, version, client_container=None):
|
||||||
directory = Path(directory).resolve(strict=True)
|
directory = Path(directory).resolve(strict=True)
|
||||||
require(channel == 'development', 'Only the persistent DEV instance supports deployment')
|
require(channel == 'development', 'Only the persistent DEV instance supports deployment')
|
||||||
cfg = settings(directory, channel)
|
cfg = settings(directory, channel)
|
||||||
registry = GITEA_DEV_IMAGE
|
registry = GITEA_DEV_IMAGE
|
||||||
require(image == registry + ':' + version, 'Expected the selected development version tag')
|
require(re.fullmatch(re.escape(registry) + r'@sha256:[0-9a-f]{64}', image), 'Expected an immutable registry digest')
|
||||||
require(re.fullmatch(r'[0-9a-f]{40}', commit), 'Expected a full commit SHA')
|
require(re.fullmatch(r'[0-9a-f]{40}', commit), 'Expected a full commit SHA')
|
||||||
# Serializes actual deployments, including manually started jobs and CLI use.
|
# Serializes actual deployments, including manually started jobs and CLI use.
|
||||||
with (directory / '.deploy.lock').open('a') as lock:
|
with (directory / '.deploy.lock').open('a') as lock:
|
||||||
@@ -109,7 +85,6 @@ def deployment(directory, channel, image, commit, version, client_container=None
|
|||||||
compose = ['docker', 'compose', '--project-name', cfg['RAWMARK_PROJECT'], '--env-file', str(directory / '.env'),
|
compose = ['docker', 'compose', '--project-name', cfg['RAWMARK_PROJECT'], '--env-file', str(directory / '.env'),
|
||||||
'-f', str(directory / 'compose.yml')]
|
'-f', str(directory / 'compose.yml')]
|
||||||
run(*compose, 'config', '--quiet', env=env)
|
run(*compose, 'config', '--quiet', env=env)
|
||||||
install_controls(directory)
|
|
||||||
backup = None
|
backup = None
|
||||||
if previous:
|
if previous:
|
||||||
run('docker', 'stop', '--time', '15', previous['Id'])
|
run('docker', 'stop', '--time', '15', previous['Id'])
|
||||||
@@ -173,7 +148,7 @@ def main():
|
|||||||
parser.add_argument('--version', required=True)
|
parser.add_argument('--version', required=True)
|
||||||
parser.add_argument('--client-container')
|
parser.add_argument('--client-container')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
require(args.image and args.commit, 'Development requires an image version tag and commit')
|
require(args.image and args.commit, 'Development requires an image digest and commit')
|
||||||
deployment(args.directory, args.channel, args.image, args.commit, args.version, args.client_container)
|
deployment(args.directory, args.channel, args.image, args.commit, args.version, args.client_container)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rebuild local fonts from the specified v7.9 ZIP: python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip.
|
"""Rebuild local fonts from the specified v7.9 ZIP: python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip.
|
||||||
Use --css-only to regenerate declarations from checked-in chunks.
|
|
||||||
Only font regeneration requires fonttools[woff]; application builds use the checked-in WOFF2 files.
|
Only font regeneration requires fonttools[woff]; application builds use the checked-in WOFF2 files.
|
||||||
Source: https://github.com/subframe7536/maple-font/releases/tag/v7.9
|
Source: https://github.com/subframe7536/maple-font/releases/tag/v7.9
|
||||||
"""
|
"""
|
||||||
@@ -9,14 +8,15 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
from fontTools import subset
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
|
||||||
root = Path(__file__).resolve().parents[1] / 'web/src/common/appearance'
|
root = Path(__file__).resolve().parents[1] / 'web/src/common/appearance'
|
||||||
output = root / 'fonts/maple'
|
output = root / 'fonts/maple'
|
||||||
output.mkdir(exist_ok=True)
|
output.mkdir(exist_ok=True)
|
||||||
family = 'Maple Mono Normal NL NF CN'
|
family = 'Maple Mono Normal NL NF CN'
|
||||||
if sys.argv[1:] != ['--css-only']:
|
css = ['/* Maple Mono v7.9; generated by scripts/prepare-maple-font.py. See fonts/maple/LICENSE.txt for licensing. */']
|
||||||
from fontTools import subset
|
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
|
||||||
(output / 'LICENSE.txt').write_bytes(archive.read('LICENSE.txt'))
|
(output / 'LICENSE.txt').write_bytes(archive.read('LICENSE.txt'))
|
||||||
for variant, weight, style in [('Regular', 400, 'normal'), ('Bold', 700, 'normal'), ('Italic', 400, 'italic'), ('BoldItalic', 700, 'italic')]:
|
for variant, weight, style in [('Regular', 400, 'normal'), ('Bold', 700, 'normal'), ('Italic', 400, 'italic'), ('BoldItalic', 700, 'italic')]:
|
||||||
data = archive.read('MapleMonoNormalNL-NF-CN-' + variant + '.ttf')
|
data = archive.read('MapleMonoNormalNL-NF-CN-' + variant + '.ttf')
|
||||||
@@ -34,12 +34,5 @@ if sys.argv[1:] != ['--css-only']:
|
|||||||
font.flavor = 'woff2'
|
font.flavor = 'woff2'
|
||||||
name = f'{variant.lower()}-{block:02x}.woff2'
|
name = f'{variant.lower()}-{block:02x}.woff2'
|
||||||
font.save(output / name)
|
font.save(output / name)
|
||||||
css = ['/* Maple Mono v7.9; generated by scripts/prepare-maple-font.py. See fonts/maple/LICENSE.txt for licensing. */']
|
css.append(f"@font-face {{ font-family: '{family}'; font-style: {style}; font-weight: {weight}; font-display: block; src: url('./fonts/maple/{name}') format('woff2'); unicode-range: U+{block * 4096:X}-{(block + 1) * 4096 - 1:X}; }}")
|
||||||
for variant, weight, style in [('regular', 400, 'normal'), ('bold', 700, 'normal'), ('italic', 400, 'italic'), ('bolditalic', 700, 'italic')]:
|
|
||||||
files = sorted(output.glob(f'{variant}-*.woff2'))
|
|
||||||
if not files:
|
|
||||||
raise ValueError(f'Missing checked-in font chunks: {variant}')
|
|
||||||
for file in files:
|
|
||||||
block = int(file.stem.split('-')[1], 16)
|
|
||||||
css.append(f"@font-face {{ font-family: '{family}'; font-style: {style}; font-weight: {weight}; font-display: swap; src: url('./fonts/maple/{file.name}') format('woff2'); unicode-range: U+{block * 4096:X}-{(block + 1) * 4096 - 1:X}; }}")
|
|
||||||
(root / 'maple-font.css').write_text('\n'.join(css) + '\n')
|
(root / 'maple-font.css').write_text('\n'.join(css) + '\n')
|
||||||
+2
-2
@@ -532,7 +532,7 @@ def promote_development(local_image):
|
|||||||
input=token, text=True, check=True)
|
input=token, text=True, check=True)
|
||||||
existing = registry_digest(GITEA_DEV_IMAGE, build['version'])
|
existing = registry_digest(GITEA_DEV_IMAGE, build['version'])
|
||||||
if existing:
|
if existing:
|
||||||
reference = GITEA_DEV_IMAGE + ':' + build['version']
|
reference = GITEA_DEV_IMAGE + '@' + existing
|
||||||
run('docker', 'pull', reference)
|
run('docker', 'pull', reference)
|
||||||
info = json.loads(run('docker', 'image', 'inspect', reference))[0]
|
info = json.loads(run('docker', 'image', 'inspect', reference))[0]
|
||||||
labels = info['Config'].get('Labels') or {}
|
labels = info['Config'].get('Labels') or {}
|
||||||
@@ -561,7 +561,7 @@ def promote_development(local_image):
|
|||||||
run('docker', 'buildx', 'imagetools', 'create', '--prefer-index=false', '--tag',
|
run('docker', 'buildx', 'imagetools', 'create', '--prefer-index=false', '--tag',
|
||||||
GITEA_DEV_IMAGE + ':' + tag, GITEA_DEV_IMAGE + '@' + value)
|
GITEA_DEV_IMAGE + ':' + tag, GITEA_DEV_IMAGE + '@' + value)
|
||||||
require(registry_digest(GITEA_DEV_IMAGE, tag) == value, 'Development tag digest changed')
|
require(registry_digest(GITEA_DEV_IMAGE, tag) == value, 'Development tag digest changed')
|
||||||
output('image', GITEA_DEV_IMAGE + ':' + build['version'])
|
output('image', GITEA_DEV_IMAGE + '@' + value)
|
||||||
output('version', build['version'])
|
output('version', build['version'])
|
||||||
finally:
|
finally:
|
||||||
run('docker', 'image', 'rm', candidate)
|
run('docker', 'image', 'rm', candidate)
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
(self.directory / '.env').write_text(
|
(self.directory / '.env').write_text(
|
||||||
f'RAWMARK_CHANNEL=development\nRAWMARK_PROJECT=rawmark-ci-deploy\n'
|
f'RAWMARK_CHANNEL=development\nRAWMARK_PROJECT=rawmark-ci-deploy\n'
|
||||||
f'RAWMARK_BIND=127.0.0.1\nRAWMARK_PORT=21899\nRAWMARK_DATA_DIR={self.directory}/data\n')
|
f'RAWMARK_BIND=127.0.0.1\nRAWMARK_PORT=21899\nRAWMARK_DATA_DIR={self.directory}/data\n')
|
||||||
self.image = release.GITEA_DEV_IMAGE + ':0.1.1'
|
self.image = release.GITEA_DEV_IMAGE + '@' + DIGEST
|
||||||
self.previous = {'Id': 'old-id', 'Image': 'old-image', 'State': {'Running': True},
|
self.previous = {'Id': 'old-id', 'Image': 'old-image', 'State': {'Running': True},
|
||||||
'Mounts': [{'Destination': '/data', 'Type': 'bind', 'Source': str(self.directory / 'data')}]}
|
'Mounts': [{'Destination': '/data', 'Type': 'bind', 'Source': str(self.directory / 'data')}]}
|
||||||
self.image_info = {'Id': 'new-image', 'Config': {'User': '65532:65532', 'Labels': {
|
self.image_info = {'Id': 'new-image', 'Config': {'User': '65532:65532', 'Labels': {
|
||||||
@@ -236,24 +236,6 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
'test': {'NetworkID': 'network', 'IPAddress': '127.0.0.1'}}}}])
|
'test': {'NetworkID': 'network', 'IPAddress': '127.0.0.1'}}}}])
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def test_existing_verified_build_outputs_a_tag(self):
|
|
||||||
build={'commit':SHA,'version':'0.1.1'}
|
|
||||||
info=json.dumps([{'Config':{'Labels':{'org.opencontainers.image.revision':SHA,
|
|
||||||
'org.opencontainers.image.version':'0.1.1'}}}])
|
|
||||||
with patch.object(release,'verify_bundle',return_value=build), \
|
|
||||||
patch.object(release.subprocess,'run'), patch.object(release,'registry_digest',return_value=DIGEST), \
|
|
||||||
patch.object(release,'run',return_value=info) as run, patch.object(release,'output') as output:
|
|
||||||
release.promote_development('synthetic')
|
|
||||||
output.assert_any_call('image',release.GITEA_DEV_IMAGE+':0.1.1')
|
|
||||||
self.assertEqual(run.call_args_list[0].args,('docker','pull',release.GITEA_DEV_IMAGE+':0.1.1'))
|
|
||||||
|
|
||||||
def test_digest_and_other_version_tags_cannot_deploy(self):
|
|
||||||
for image in (release.GITEA_DEV_IMAGE + '@' + DIGEST, release.GITEA_DEV_IMAGE + ':other'):
|
|
||||||
with self.subTest(image=image), patch.object(deploy, 'run') as docker:
|
|
||||||
with self.assertRaisesRegex(ValueError, 'version tag'):
|
|
||||||
deploy.deployment(self.directory, 'development', image, SHA, '0.1.1')
|
|
||||||
docker.assert_not_called()
|
|
||||||
|
|
||||||
def test_channel_mismatch_cannot_touch_docker(self):
|
def test_channel_mismatch_cannot_touch_docker(self):
|
||||||
with patch.object(deploy, 'run') as docker:
|
with patch.object(deploy, 'run') as docker:
|
||||||
with self.assertRaisesRegex(ValueError, 'channel'):
|
with self.assertRaisesRegex(ValueError, 'channel'):
|
||||||
@@ -295,16 +277,6 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
start = next(i for i,c in enumerate(self.commands) if c[:2] == ('docker', 'compose') and 'up' in c)
|
start = next(i for i,c in enumerate(self.commands) if c[:2] == ('docker', 'compose') and 'up' in c)
|
||||||
self.assertLess(stop, start)
|
self.assertLess(stop, start)
|
||||||
self.assertIn('RAWMARK_IMAGE=' + self.image, (self.directory / '.env').read_text())
|
self.assertIn('RAWMARK_IMAGE=' + self.image, (self.directory / '.env').read_text())
|
||||||
self.assertTrue((self.directory / 'start.sh').stat().st_mode & 0o111)
|
|
||||||
self.assertTrue((self.directory / 'stop.sh').stat().st_mode & 0o111)
|
|
||||||
|
|
||||||
def test_custom_control_script_is_preserved_before_stopping(self):
|
|
||||||
(self.directory / 'stop.sh').write_text('custom behavior\n')
|
|
||||||
with patch.object(deploy, 'run', side_effect=self.command):
|
|
||||||
with self.assertRaisesRegex(ValueError, 'Locally modified'):
|
|
||||||
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
|
||||||
self.assertFalse(any(c[:2] == ('docker', 'stop') for c in self.commands))
|
|
||||||
self.assertEqual((self.directory / 'stop.sh').read_text(), 'custom behavior\n')
|
|
||||||
|
|
||||||
def test_existing_data_is_backed_up_without_a_container(self):
|
def test_existing_data_is_backed_up_without_a_container(self):
|
||||||
def without_container(*args, **kwargs):
|
def without_container(*args, **kwargs):
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
"""Verify instance controls with synthetic files and no container mutations."""
|
|
||||||
|
|
||||||
import fcntl
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
||||||
import deploy
|
|
||||||
|
|
||||||
|
|
||||||
SOURCES = Path(__file__).resolve().parents[2] / 'deploy'
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceControlTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.tmp = tempfile.TemporaryDirectory(prefix='rawmark controls ')
|
|
||||||
self.addCleanup(self.tmp.cleanup)
|
|
||||||
self.directory = Path(self.tmp.name)
|
|
||||||
self.bin = self.directory / 'bin'
|
|
||||||
self.bin.mkdir()
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
shutil.copy2(SOURCES / name, self.directory / name)
|
|
||||||
(self.directory / 'compose.yml').write_text(
|
|
||||||
'name: ${RAWMARK_PROJECT:?missing project}\nservices:\n app:\n'
|
|
||||||
' image: ${RAWMARK_IMAGE:?missing image}\n environment:\n'
|
|
||||||
' APP_SETTING: ${APP_SETTING:?missing setting}\n'
|
|
||||||
' volumes:\n - ${RAWMARK_DATA_DIR:?missing data}:/data\n')
|
|
||||||
(self.directory / '.env').write_text(
|
|
||||||
'RAWMARK_PROJECT=rawmark-controls\nRAWMARK_IMAGE=example.invalid/verified:old\n'
|
|
||||||
f'APP_SETTING=from-dotenv\nRAWMARK_DATA_DIR={self.directory}/data\n')
|
|
||||||
(self.directory / 'data').mkdir()
|
|
||||||
(self.directory / 'data/keep').write_text('persistent')
|
|
||||||
(self.bin / 'docker').write_text(f'#!{sys.executable}\n' + '''
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
directory = Path(__file__).resolve().parents[1]
|
|
||||||
args = sys.argv[1:]
|
|
||||||
with (directory / 'calls.jsonl').open('a') as log:
|
|
||||||
log.write(json.dumps({'args': args, 'env': dict(os.environ)}) + '\\n')
|
|
||||||
if 'config' in args and (directory / 'real-docker').exists():
|
|
||||||
real = (directory / 'real-docker').read_text()
|
|
||||||
result = subprocess.run([real, *args[:-1], '--format', 'json'], capture_output=True, text=True)
|
|
||||||
(directory / 'resolved.json').write_text(result.stdout)
|
|
||||||
sys.stderr.write(result.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
status = directory / 'failure-code'
|
|
||||||
if 'config' not in args and (directory / 'action-failure-code').exists():
|
|
||||||
status = directory / 'action-failure-code'
|
|
||||||
sys.exit(int(status.read_text()) if status.exists() else 0)
|
|
||||||
''')
|
|
||||||
(self.bin / 'docker').chmod(0o755)
|
|
||||||
|
|
||||||
def invoke(self, name, **environment):
|
|
||||||
return subprocess.run(['bash', str(self.directory / name)], cwd='/',
|
|
||||||
env={**os.environ, 'PATH': str(self.bin) + ':' + os.environ['PATH'], **environment},
|
|
||||||
capture_output=True, text=True)
|
|
||||||
|
|
||||||
def calls(self):
|
|
||||||
path = self.directory / 'calls.jsonl'
|
|
||||||
return [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []
|
|
||||||
|
|
||||||
def test_start_and_stop_use_instance_files_and_preserve_data(self):
|
|
||||||
(self.directory / 'images.env').write_text('RAWMARK_IMAGE=example.invalid/verified:new\n')
|
|
||||||
for name, action in (('start.sh', ['up', '-d', '--no-build', '--pull', 'never']),
|
|
||||||
('stop.sh', ['stop'])):
|
|
||||||
with self.subTest(name=name):
|
|
||||||
result = self.invoke(name, RAWMARK_PROJECT='other-project', RAWMARK_IMAGE='unverified',
|
|
||||||
RAWMARK_DATA_DIR='/other-data', APP_SETTING='caller',
|
|
||||||
COMPOSE_FILE='/wrong.yml', COMPOSE_PROJECT_NAME='other',
|
|
||||||
COMPOSE_PROFILES='extra', COMPOSE_DISABLE_ENV_FILE='true')
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
call = self.calls()[-1]
|
|
||||||
self.assertEqual(call['args'][-len(action):], action)
|
|
||||||
files = [call['args'][i + 1] for i, arg in enumerate(call['args']) if arg == '--env-file']
|
|
||||||
self.assertEqual(files, [str(self.directory / '.env'), str(self.directory / 'images.env')])
|
|
||||||
self.assertEqual(call['args'][call['args'].index('--project-directory') + 1], str(self.directory))
|
|
||||||
self.assertFalse(any(key.startswith(('RAWMARK_', 'COMPOSE_')) or key == 'APP_SETTING'
|
|
||||||
for key in call['env']))
|
|
||||||
self.assertEqual((self.directory / 'data/keep').read_text(), 'persistent')
|
|
||||||
|
|
||||||
def test_docker_client_configuration_is_preserved(self):
|
|
||||||
result = self.invoke('stop.sh', DOCKER_CONFIG='/private/docker-config')
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
self.assertEqual(self.calls()[-1]['env']['DOCKER_CONFIG'], '/private/docker-config')
|
|
||||||
|
|
||||||
def test_pending_blocks_start_but_allows_stop(self):
|
|
||||||
(self.directory / 'deployment.pending.json').write_text('{}')
|
|
||||||
self.assertNotEqual(self.invoke('start.sh').returncode, 0)
|
|
||||||
self.assertEqual(self.calls(), [])
|
|
||||||
self.assertEqual(self.invoke('stop.sh').returncode, 0)
|
|
||||||
self.assertEqual(self.calls()[-1]['args'][-1], 'stop')
|
|
||||||
|
|
||||||
def test_lock_blocks_both_controls(self):
|
|
||||||
with (self.directory / '.deploy.lock').open('a') as lock:
|
|
||||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
self.assertNotEqual(self.invoke(name).returncode, 0)
|
|
||||||
self.assertEqual(self.calls(), [])
|
|
||||||
|
|
||||||
def test_invalid_compose_count_rejects_before_docker(self):
|
|
||||||
(self.directory / 'compose.yaml').write_text('services: {}')
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
self.assertNotEqual(self.invoke(name).returncode, 0)
|
|
||||||
self.assertEqual(self.calls(), [])
|
|
||||||
|
|
||||||
def test_config_failure_prevents_action_and_propagates_status(self):
|
|
||||||
(self.directory / 'failure-code').write_text('17')
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
result = self.invoke(name)
|
|
||||||
self.assertEqual(result.returncode, 17)
|
|
||||||
self.assertEqual(self.calls()[-1]['args'][-2:], ['config', '--quiet'])
|
|
||||||
|
|
||||||
def test_action_failure_propagates_without_success_message(self):
|
|
||||||
(self.directory / 'action-failure-code').write_text('19')
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
result = self.invoke(name)
|
|
||||||
self.assertEqual(result.returncode, 19)
|
|
||||||
self.assertEqual(result.stdout, '')
|
|
||||||
|
|
||||||
def test_real_compose_loads_dotenv_and_image_override_without_caller_overrides(self):
|
|
||||||
docker = shutil.which('docker')
|
|
||||||
if not docker or subprocess.run([docker, 'compose', 'version'], capture_output=True).returncode:
|
|
||||||
self.skipTest('Docker Compose is not installed')
|
|
||||||
(self.directory / 'real-docker').write_text(docker)
|
|
||||||
(self.directory / 'images.env').write_text('RAWMARK_IMAGE=example.invalid/verified:new\n')
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
result = self.invoke(name, RAWMARK_PROJECT='wrong', RAWMARK_IMAGE='wrong',
|
|
||||||
RAWMARK_DATA_DIR='/wrong', APP_SETTING='wrong')
|
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
|
||||||
resolved = json.loads((self.directory / 'resolved.json').read_text())
|
|
||||||
self.assertEqual(resolved['name'], 'rawmark-controls')
|
|
||||||
self.assertEqual(resolved['services']['app']['image'], 'example.invalid/verified:new')
|
|
||||||
self.assertEqual(resolved['services']['app']['environment']['APP_SETTING'], 'from-dotenv')
|
|
||||||
self.assertEqual(resolved['services']['app']['volumes'][0]['source'], str(self.directory / 'data'))
|
|
||||||
|
|
||||||
|
|
||||||
class ControlInstallationTests(unittest.TestCase):
|
|
||||||
def test_fresh_install_and_update_preserve_local_edits(self):
|
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
|
||||||
directory = Path(temporary)
|
|
||||||
deploy.install_controls(directory)
|
|
||||||
for name in ('start.sh', 'stop.sh'):
|
|
||||||
self.assertEqual((directory / name).stat().st_mode & 0o777, 0o755)
|
|
||||||
# A previously managed version can be updated using its recorded digest.
|
|
||||||
(directory / 'start.sh').write_text('previous managed version')
|
|
||||||
receipt = json.loads((directory / '.control-scripts.json').read_text())
|
|
||||||
receipt['start.sh'] = hashlib.sha256((directory / 'start.sh').read_bytes()).hexdigest()
|
|
||||||
(directory / '.control-scripts.json').write_text(json.dumps(receipt))
|
|
||||||
deploy.install_controls(directory)
|
|
||||||
self.assertEqual((directory / 'start.sh').read_bytes(), (SOURCES / 'start.sh').read_bytes())
|
|
||||||
(directory / 'stop.sh').write_text('custom user behavior')
|
|
||||||
with self.assertRaisesRegex(ValueError, 'Locally modified'):
|
|
||||||
deploy.install_controls(directory)
|
|
||||||
self.assertEqual((directory / 'stop.sh').read_text(), 'custom user behavior')
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-rm-fonts="loading">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-rm-fonts="loading">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
|||||||
@@ -628,18 +628,20 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* opacity preserves layout and font requests; hide fallback fonts while waiting, including menus teleported to body. */
|
||||||
|
:root:is([data-rm-fonts='loading'], [data-rm-fonts='error']) body > :not(.rm-font-error) {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.rm-font-error {
|
.rm-font-error {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
inset: 0;
|
||||||
right: 16px;
|
display: flex;
|
||||||
bottom: 16px;
|
flex-direction: column;
|
||||||
max-width: min(360px, calc(100vw - 64px));
|
align-items: center;
|
||||||
padding: 16px;
|
justify-content: center;
|
||||||
border: 1px solid var(--rm-divider);
|
padding: 24px;
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--rm-bg);
|
|
||||||
color: var(--rm-text);
|
color: var(--rm-text);
|
||||||
box-shadow: var(--rm-shadow);
|
|
||||||
font:
|
font:
|
||||||
16px/1.5 system-ui,
|
16px/1.5 system-ui,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
|
|||||||
@@ -2,66 +2,96 @@
|
|||||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||||
import { watchFontLoading } from './font-loading'
|
import { watchFontLoading } from './font-loading'
|
||||||
|
|
||||||
function face(overrides = {}) {
|
|
||||||
return { family: 'Maple Mono Normal NL NF CN', weight: '400', style: 'normal', status: 'unloaded', load: vi.fn().mockResolvedValue(undefined), ...overrides }
|
|
||||||
}
|
|
||||||
class Fonts extends EventTarget {
|
class Fonts extends EventTarget {
|
||||||
faces = [face()];
|
status = 'loading'
|
||||||
|
faces: { status: string }[] = []
|
||||||
|
resolve!: () => void
|
||||||
|
ready = this.pending()
|
||||||
|
pending() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
this.resolve = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return this.faces[Symbol.iterator]()
|
return this.faces[Symbol.iterator]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
this.status = 'loaded'; this.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.status = 'loading'; this.ready = this.pending(); this.dispatchEvent(new Event('loading'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let fonts: Fonts
|
let fonts: Fonts
|
||||||
let frame: FrameRequestCallback
|
let frame: FrameRequestCallback
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(document, 'fonts')
|
const descriptor = Object.getOwnPropertyDescriptor(document, 'fonts')
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers()
|
|
||||||
document.body.innerHTML = '<div id="app">正文</div>'
|
document.body.innerHTML = '<div id="app">正文</div>'
|
||||||
fonts = new Fonts()
|
fonts = new Fonts()
|
||||||
Object.defineProperty(document, 'fonts', { configurable: true, value: fonts })
|
Object.defineProperty(document, 'fonts', { configurable: true, value: fonts })
|
||||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||||
frame = callback; return 1
|
frame = callback; return 1
|
||||||
})
|
})
|
||||||
|
watchFontLoading()
|
||||||
})
|
})
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers(); vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
|
delete document.documentElement.dataset.rmFonts
|
||||||
document.body.innerHTML = ''
|
document.body.innerHTML = ''
|
||||||
if (descriptor) Object.defineProperty(document, 'fonts', descriptor)
|
if (descriptor) Object.defineProperty(document, 'fonts', descriptor)
|
||||||
else Reflect.deleteProperty(document, 'fonts')
|
else Reflect.deleteProperty(document, 'fonts')
|
||||||
})
|
})
|
||||||
it('首屏与后续字体加载均不隐藏或禁用应用,预下载不阻塞首屏', async () => {
|
|
||||||
watchFontLoading()
|
it('挂载后的字体就绪前保持隐藏,等待结束才显示', async () => {
|
||||||
expect(fonts.faces[0]!.load).not.toHaveBeenCalled()
|
expect(document.documentElement.dataset.rmFonts).toBe('loading')
|
||||||
fonts.dispatchEvent(new Event('loading'))
|
|
||||||
expect(document.documentElement.dataset.rmFonts).toBeUndefined()
|
|
||||||
expect(getComputedStyle(document.getElementById('app')!).pointerEvents).not.toBe('none')
|
|
||||||
frame(0)
|
frame(0)
|
||||||
expect(fonts.faces[0]!.load).not.toHaveBeenCalled()
|
await Promise.resolve()
|
||||||
await vi.advanceTimersByTimeAsync(0)
|
expect(document.documentElement.dataset.rmFonts).toBe('loading')
|
||||||
expect(fonts.faces[0]!.load).toHaveBeenCalledOnce()
|
fonts.finish()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('ready')
|
||||||
})
|
})
|
||||||
it('只预下载常规Maple,每批两个,单片失败仍继续且不重复启动', async () => {
|
|
||||||
let finish!: () => void
|
it('字体已在缓存中时直接显示,不依赖再次触发 loading 事件', async () => {
|
||||||
fonts.faces = [face({ family: '\'Maple Mono Normal NL NF CN\'', load: vi.fn(() => new Promise<void>((resolve) => {
|
fonts.finish()
|
||||||
finish = resolve
|
frame(0)
|
||||||
})) }), face({ load: vi.fn().mockRejectedValue(new Error('offline')) }), face(), face({ weight: '700' }), face({ style: 'italic' }), face({ family: 'Other font' })]
|
await Promise.resolve()
|
||||||
watchFontLoading(); frame(0)
|
expect(document.documentElement.dataset.rmFonts).toBe('ready')
|
||||||
await vi.advanceTimersByTimeAsync(0)
|
|
||||||
expect(fonts.faces[0]!.load).toHaveBeenCalledOnce()
|
|
||||||
expect(fonts.faces[1]!.load).toHaveBeenCalledOnce()
|
|
||||||
expect(fonts.faces[2]!.load).not.toHaveBeenCalled()
|
|
||||||
finish(); await vi.advanceTimersByTimeAsync(0)
|
|
||||||
expect(fonts.faces[2]!.load).toHaveBeenCalledOnce()
|
|
||||||
for (const font of fonts.faces.slice(3)) expect(font.load).not.toHaveBeenCalled()
|
|
||||||
watchFontLoading(); await vi.advanceTimersByTimeAsync(0)
|
|
||||||
expect(fonts.faces[0]!.load).toHaveBeenCalledOnce()
|
|
||||||
expect(document.querySelectorAll('[role="alert"]')).toHaveLength(1)
|
|
||||||
})
|
})
|
||||||
it('字体失败只提示一次,保留页面和刷新重试入口', () => {
|
|
||||||
watchFontLoading()
|
it('切换笔记或字形需要新字体时重新等待,不用旧一轮完成信号提前显示', async () => {
|
||||||
fonts.dispatchEvent(new Event('loadingerror')); fonts.dispatchEvent(new Event('loadingerror'))
|
frame(0)
|
||||||
|
fonts.finish()
|
||||||
|
fonts.load()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('loading')
|
||||||
|
fonts.finish()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('ready')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('字体失败显示可刷新提示,之后 ready 完成和其他字体加载不能显示备用字体', async () => {
|
||||||
|
frame(0)
|
||||||
|
fonts.dispatchEvent(new Event('loadingerror'))
|
||||||
|
fonts.dispatchEvent(new Event('loadingerror'))
|
||||||
|
fonts.finish()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('error')
|
||||||
expect(document.querySelectorAll('[role="alert"]')).toHaveLength(1)
|
expect(document.querySelectorAll('[role="alert"]')).toHaveLength(1)
|
||||||
expect(document.querySelector('[role="alert"]')!.textContent).toContain('Using fallback fonts')
|
expect(document.querySelector('[role="alert"]')!.textContent).toContain('Fonts could not be loaded')
|
||||||
expect(document.querySelector('[role="alert"] button')!.textContent).toBe('Refresh page')
|
expect(document.querySelector('[role="alert"] button')!.textContent).toBe('Refresh page')
|
||||||
expect(document.documentElement.dataset.rmFonts).toBeUndefined()
|
fonts.load()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('error')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('开始监听前已有字体失败,也不能被 resolved ready 当作成功', async () => {
|
||||||
|
fonts.faces = [{ status: 'error' }]
|
||||||
|
fonts.finish()
|
||||||
|
frame(0)
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(document.documentElement.dataset.rmFonts).toBe('error')
|
||||||
|
expect(document.querySelector('[role="alert"]')).not.toBeNull()
|
||||||
})
|
})
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
import { t as translate } from '@@/i18n/runtime'
|
import { t as translate } from '@@/i18n/runtime'
|
||||||
|
// Use the font set requested by actual browser layout, covering initial render, note switches, and later glyphs.
|
||||||
const started = new WeakSet<FontFaceSet>()
|
|
||||||
|
|
||||||
// Fonts may finish after interaction starts; never hide or disable the application.
|
|
||||||
export function watchFontLoading() {
|
export function watchFontLoading() {
|
||||||
|
const root = document.documentElement
|
||||||
const fonts = document.fonts
|
const fonts = document.fonts
|
||||||
if (started.has(fonts)) return
|
root.dataset.rmFonts = 'loading'
|
||||||
started.add(fonts)
|
|
||||||
let reported = false
|
|
||||||
function failed() {
|
function failed() {
|
||||||
if (reported) return
|
if (root.dataset.rmFonts === 'error') return
|
||||||
reported = true
|
root.dataset.rmFonts = 'error'
|
||||||
const panel = document.createElement('div')
|
const panel = document.createElement('div')
|
||||||
panel.className = 'rm-font-error'
|
panel.className = 'rm-font-error'
|
||||||
panel.setAttribute('role', 'alert')
|
panel.setAttribute('role', 'alert')
|
||||||
@@ -22,20 +19,24 @@ export function watchFontLoading() {
|
|||||||
panel.append(message, retry)
|
panel.append(message, retry)
|
||||||
document.body.append(panel)
|
document.body.append(panel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ready() {
|
||||||
|
if (root.dataset.rmFonts === 'error') return
|
||||||
|
// fonts.ready also resolves after download failure; failure must not count as fonts being ready.
|
||||||
|
if ([...fonts].some(font => font.status === 'error')) failed()
|
||||||
|
else if (fonts.status === 'loaded') root.dataset.rmFonts = 'ready'
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts.addEventListener('loading', () => {
|
||||||
|
if (root.dataset.rmFonts === 'error') return
|
||||||
|
root.dataset.rmFonts = 'loading'
|
||||||
|
void fonts.ready.then(ready)
|
||||||
|
})
|
||||||
fonts.addEventListener('loadingerror', failed)
|
fonts.addEventListener('loadingerror', failed)
|
||||||
|
|
||||||
async function preload() {
|
// Trigger layout after mounting so the wait covers fonts used by the current text.
|
||||||
const regular = [...fonts].filter(face => face.family.replace(/['"]/g, '') === 'Maple Mono Normal NL NF CN' && face.style === 'normal' && face.weight === '400')
|
|
||||||
for (let i = 0; i < regular.length; i += 2) {
|
|
||||||
const results = await Promise.allSettled(regular.slice(i, i + 2).map(face => face.load()))
|
|
||||||
if (results.some(result => result.status === 'rejected')) failed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Yield a paint before scheduling background downloads, including in the standalone demo.
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
document.body.getBoundingClientRect()
|
||||||
if ([...fonts].some(face => face.status === 'error')) failed()
|
void fonts.ready.then(ready)
|
||||||
void preload()
|
|
||||||
}, 0)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+113
-113
@@ -3,7 +3,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/6b5e37b52d26eeac.woff2) format('woff2');
|
src: url(./fonts/6b5e37b52d26eeac.woff2) format('woff2');
|
||||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/eb83d8d40af4411f.woff2) format('woff2');
|
src: url(./fonts/eb83d8d40af4411f.woff2) format('woff2');
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/99e400330f36ae77.woff2) format('woff2');
|
src: url(./fonts/99e400330f36ae77.woff2) format('woff2');
|
||||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/c4064400db3750ce.woff2) format('woff2');
|
src: url(./fonts/c4064400db3750ce.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b19c2080652a3c2b.woff2) format('woff2');
|
src: url(./fonts/b19c2080652a3c2b.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 600;
|
font-weight: 400 600;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/cc27b30471250057.woff2) format('woff2');
|
src: url(./fonts/cc27b30471250057.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/842459da81f909f2.woff2) format('woff2');
|
src: url(./fonts/842459da81f909f2.woff2) format('woff2');
|
||||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/9b2d027172a3481c.woff2) format('woff2');
|
src: url(./fonts/9b2d027172a3481c.woff2) format('woff2');
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/ed7393f42d27db3d.woff2) format('woff2');
|
src: url(./fonts/ed7393f42d27db3d.woff2) format('woff2');
|
||||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/1f3ea86e40d947e2.woff2) format('woff2');
|
src: url(./fonts/1f3ea86e40d947e2.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/ee75508933e6fd45.woff2) format('woff2');
|
src: url(./fonts/ee75508933e6fd45.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
font-family: 'Manrope';
|
font-family: 'Manrope';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4872f2a6dc9a8250.woff2) format('woff2');
|
src: url(./fonts/4872f2a6dc9a8250.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/7b3ddaeea40a0fe5.woff2) format('woff2');
|
src: url(./fonts/7b3ddaeea40a0fe5.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+1f1e9-1f1f5, U+1f1f7-1f1ff, U+1f21a, U+1f232, U+1f234-1f237, U+1f250-1f251, U+1f300, U+1f302-1f308, U+1f30a-1f311,
|
U+1f1e9-1f1f5, U+1f1f7-1f1ff, U+1f21a, U+1f232, U+1f234-1f237, U+1f250-1f251, U+1f300, U+1f302-1f308, U+1f30a-1f311,
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/9af011928c669ba6.woff2) format('woff2');
|
src: url(./fonts/9af011928c669ba6.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d,
|
U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d,
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/5f6c87a981ef2def.woff2) format('woff2');
|
src: url(./fonts/5f6c87a981ef2def.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+f0a7, U+f0b2, U+f0b7, U+f0c9, U+f0d8, U+f0da, U+f0dc-f0dd, U+f0e0, U+f0e6, U+f0eb, U+f0fc, U+f101, U+f104-f105,
|
U+f0a7, U+f0b2, U+f0b7, U+f0c9, U+f0d8, U+f0da, U+f0dc-f0dd, U+f0e0, U+f0e6, U+f0eb, U+f0fc, U+f101, U+f104-f105,
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/8953b4246f39fce9.woff2) format('woff2');
|
src: url(./fonts/8953b4246f39fce9.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9f83, U+9f85-9f8d, U+9f90-9f91, U+9f94-9f96, U+9f98, U+9f9b-9f9c, U+9f9e, U+9fa0, U+9fa2, U+9ff0-9fff, U+a001,
|
U+9f83, U+9f85-9f8d, U+9f90-9f91, U+9f94-9f96, U+9f98, U+9f9b-9f9c, U+9f9e, U+9fa0, U+9fa2, U+9ff0-9fff, U+a001,
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/e55b19a3912a75d1.woff2) format('woff2');
|
src: url(./fonts/e55b19a3912a75d1.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9e30-9e33, U+9e35-9e3b, U+9e3e, U+9e40-9e44, U+9e46-9e4e, U+9e51, U+9e53, U+9e55-9e58, U+9e5a-9e5c, U+9e5e-9e63,
|
U+9e30-9e33, U+9e35-9e3b, U+9e3e, U+9e40-9e44, U+9e46-9e4e, U+9e51, U+9e53, U+9e55-9e58, U+9e5a-9e5c, U+9e5e-9e63,
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/935e2cf08e92673e.woff2) format('woff2');
|
src: url(./fonts/935e2cf08e92673e.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9c82-9c83, U+9c85-9c8c, U+9c8e-9c92, U+9c94-9c9b, U+9c9e-9ca3, U+9ca5-9ca7, U+9ca9, U+9cab, U+9cad-9cae,
|
U+9c82-9c83, U+9c85-9c8c, U+9c8e-9c92, U+9c94-9c9b, U+9c9e-9ca3, U+9ca5-9ca7, U+9ca9, U+9cab, U+9cad-9cae,
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b5ae0757f6ecaa47.woff2) format('woff2');
|
src: url(./fonts/b5ae0757f6ecaa47.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9a80, U+9a83, U+9a85, U+9a88-9a8a, U+9a8d-9a8e, U+9a90, U+9a92-9a93, U+9a95-9a96, U+9a98-9a99, U+9a9b-9aa2,
|
U+9a80, U+9a83, U+9a85, U+9a88-9a8a, U+9a8d-9a8e, U+9a90, U+9a92-9a93, U+9a95-9a96, U+9a98-9a99, U+9a9b-9aa2,
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4e47f619496a388f.woff2) format('woff2');
|
src: url(./fonts/4e47f619496a388f.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+98dd, U+98e1-98e2, U+98e7-98ea, U+98ec, U+98ee-98ef, U+98f2, U+98f4, U+98fc-98fe, U+9903, U+9905, U+9908, U+990a,
|
U+98dd, U+98e1-98e2, U+98e7-98ea, U+98ec, U+98ee-98ef, U+98f2, U+98f4, U+98fc-98fe, U+9903, U+9905, U+9908, U+990a,
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/3bc0783456e12fcc.woff2) format('woff2');
|
src: url(./fonts/3bc0783456e12fcc.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+975b-975c, U+9763, U+9765-9766, U+976c-976d, U+9773, U+9776, U+977a, U+977c, U+9784-9785, U+978e-978f,
|
U+975b-975c, U+9763, U+9765-9766, U+976c-976d, U+9773, U+9776, U+977a, U+977c, U+9784-9785, U+978e-978f,
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4168a698bef8858e.woff2) format('woff2');
|
src: url(./fonts/4168a698bef8858e.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+95c4-95ca, U+95cc-95cd, U+95d4-95d6, U+95d8, U+95e1-95e2, U+95e9, U+95f0-95f1, U+95f3, U+95f6, U+95fc,
|
U+95c4-95ca, U+95cc-95cd, U+95d4-95d6, U+95d8, U+95e1-95e2, U+95e9, U+95f0-95f1, U+95f3, U+95f6, U+95fc,
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/7a0b612893faf498.woff2) format('woff2');
|
src: url(./fonts/7a0b612893faf498.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+94f5, U+94f7, U+94f9, U+94fb-94fd, U+94ff, U+9503-9504, U+9506-9507, U+9509-950a, U+950d-950f, U+9511-9518,
|
U+94f5, U+94f7, U+94f9, U+94fb-94fd, U+94ff, U+9503-9504, U+9506-9507, U+9509-950a, U+950d-950f, U+9511-9518,
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/95d37f56ac503320.woff2) format('woff2');
|
src: url(./fonts/95d37f56ac503320.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+941c-942b, U+942d-942e, U+9432-9433, U+9435, U+9438, U+943a, U+943e, U+9444, U+944a, U+9451-9452, U+945a,
|
U+941c-942b, U+942d-942e, U+9432-9433, U+9435, U+9438, U+943a, U+943e, U+9444, U+944a, U+9451-9452, U+945a,
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/7e74d2ffc02d78d0.woff2) format('woff2');
|
src: url(./fonts/7e74d2ffc02d78d0.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+92ec-92ed, U+92f0, U+92f3, U+92f8, U+92fc, U+9304, U+9306, U+9310, U+9312, U+9315, U+9318, U+931a, U+931e,
|
U+92ec-92ed, U+92f0, U+92f3, U+92f8, U+92fc, U+9304, U+9306, U+9310, U+9312, U+9315, U+9318, U+931a, U+931e,
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/70a0301ffa863394.woff2) format('woff2');
|
src: url(./fonts/70a0301ffa863394.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9163-9164, U+9169, U+9170, U+9172, U+9174, U+9179-917a, U+917d-917e, U+9182-9183, U+9185, U+918c-918d,
|
U+9163-9164, U+9169, U+9170, U+9172, U+9174, U+9179-917a, U+917d-917e, U+9182-9183, U+9185, U+918c-918d,
|
||||||
@@ -320,7 +320,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f65e41fe796f55cb.woff2) format('woff2');
|
src: url(./fonts/f65e41fe796f55cb.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+9004, U+900b, U+9011, U+9015-9016, U+901e, U+9021, U+9026, U+902d, U+902f, U+9031, U+9035-9036, U+9039-903a,
|
U+9004, U+900b, U+9011, U+9015-9016, U+901e, U+9021, U+9026, U+902d, U+902f, U+9031, U+9035-9036, U+9039-903a,
|
||||||
@@ -336,7 +336,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/1a993e7ab856f9c8.woff2) format('woff2');
|
src: url(./fonts/1a993e7ab856f9c8.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8e41-8e42, U+8e47, U+8e49-8e4b, U+8e50-8e53, U+8e59-8e5a, U+8e5f-8e60, U+8e64, U+8e69, U+8e6c, U+8e70, U+8e74,
|
U+8e41-8e42, U+8e47, U+8e49-8e4b, U+8e50-8e53, U+8e59-8e5a, U+8e5f-8e60, U+8e64, U+8e69, U+8e6c, U+8e70, U+8e74,
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/50a18da9b4927dbb.woff2) format('woff2');
|
src: url(./fonts/50a18da9b4927dbb.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8cbd, U+8cbf-8cc4, U+8cc7-8cc8, U+8cca, U+8ccd, U+8cd1, U+8cd3, U+8cdb-8cdc, U+8cde, U+8ce0, U+8ce2-8ce4,
|
U+8cbd, U+8cbf-8cc4, U+8cc7-8cc8, U+8cca, U+8ccd, U+8cd1, U+8cd3, U+8cdb-8cdc, U+8cde, U+8ce0, U+8ce2-8ce4,
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/a806fd74a8216e5f.woff2) format('woff2');
|
src: url(./fonts/a806fd74a8216e5f.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8b80, U+8b83, U+8b8a, U+8b8c, U+8b90, U+8b93, U+8b99-8b9a, U+8ba0, U+8ba3, U+8ba5-8ba7, U+8baa-8bac, U+8bb4-8bb5,
|
U+8b80, U+8b83, U+8b8a, U+8b8c, U+8b90, U+8b93, U+8b99-8b9a, U+8ba0, U+8ba3, U+8ba5-8ba7, U+8baa-8bac, U+8bb4-8bb5,
|
||||||
@@ -383,7 +383,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/78bd32786a1b31ff.woff2) format('woff2');
|
src: url(./fonts/78bd32786a1b31ff.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8a15-8a18, U+8a1a-8a1b, U+8a1d, U+8a1f, U+8a22-8a23, U+8a25, U+8a2b, U+8a2d, U+8a31, U+8a33-8a34, U+8a36-8a38,
|
U+8a15-8a18, U+8a1a-8a1b, U+8a1d, U+8a1f, U+8a22-8a23, U+8a25, U+8a2b, U+8a2d, U+8a31, U+8a33-8a34, U+8a36-8a38,
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f44e78d8ce83767d.woff2) format('woff2');
|
src: url(./fonts/f44e78d8ce83767d.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8882, U+8884-8886, U+8888, U+888f, U+8892-8893, U+889b, U+88a2, U+88a4, U+88a6, U+88a8, U+88aa, U+88ae, U+88b1,
|
U+8882, U+8884-8886, U+8888, U+888f, U+8892-8893, U+889b, U+88a2, U+88a4, U+88a6, U+88a8, U+88aa, U+88ae, U+88b1,
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/63e1e25c88076e71.woff2) format('woff2');
|
src: url(./fonts/63e1e25c88076e71.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+86f4, U+86f8-86f9, U+86fb, U+86fe, U+8703, U+8706-870a, U+870d, U+8711-8713, U+871a, U+871e, U+8722-8723, U+8725,
|
U+86f4, U+86f8-86f9, U+86fb, U+86fe, U+8703, U+8706-870a, U+870d, U+8711-8713, U+871a, U+871e, U+8722-8723, U+8725,
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/badb08d4ed31aab3.woff2) format('woff2');
|
src: url(./fonts/badb08d4ed31aab3.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8548, U+854e, U+8553, U+8556-8557, U+8559, U+855e, U+8561, U+8564-8565, U+8568-856a, U+856d, U+856f-8570, U+8572,
|
U+8548, U+854e, U+8553, U+8556-8557, U+8559, U+855e, U+8561, U+8564-8565, U+8568-856a, U+856d, U+856f-8570, U+8572,
|
||||||
@@ -447,7 +447,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/9d0550296822bc88.woff2) format('woff2');
|
src: url(./fonts/9d0550296822bc88.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+83c5, U+83c8-83c9, U+83cb, U+83d1, U+83d3-83d6, U+83d8, U+83db, U+83dd, U+83df, U+83e1, U+83e5, U+83ea-83eb,
|
U+83c5, U+83c8-83c9, U+83cb, U+83d1, U+83d3-83d6, U+83d8, U+83db, U+83dd, U+83df, U+83e1, U+83e5, U+83ea-83eb,
|
||||||
@@ -464,7 +464,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/ea65d613de6262b7.woff2) format('woff2');
|
src: url(./fonts/ea65d613de6262b7.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+82bc, U+82be, U+82c0-82c2, U+82c4-82c8, U+82ca-82cc, U+82ce, U+82d0, U+82d2-82d3, U+82d5-82d6, U+82d8-82d9,
|
U+82bc, U+82be, U+82c0-82c2, U+82c4-82c8, U+82ca-82cc, U+82ce, U+82d0, U+82d2-82d3, U+82d5-82d6, U+82d8-82d9,
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f624e6edcc42557f.woff2) format('woff2');
|
src: url(./fonts/f624e6edcc42557f.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8166-8169, U+816b, U+816d, U+8171, U+8173-8174, U+8178, U+817c-817d, U+8182, U+8188, U+8191, U+8198-819b, U+81a0,
|
U+8166-8169, U+816b, U+816d, U+8171, U+8173-8174, U+8178, U+817c-817d, U+8182, U+8188, U+8191, U+8198-819b, U+81a0,
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/af97bfb909af51ae.woff2) format('woff2');
|
src: url(./fonts/af97bfb909af51ae.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+8016, U+8018-8019, U+801c, U+801e, U+8026-802a, U+8031, U+8034-8035, U+8037, U+8043, U+804b, U+804d, U+8052,
|
U+8016, U+8018-8019, U+801c, U+801e, U+8026-802a, U+8031, U+8034-8035, U+8037, U+8043, U+804b, U+804d, U+8052,
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/84a684f5b97032b6.woff2) format('woff2');
|
src: url(./fonts/84a684f5b97032b6.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7ef0-7ef2, U+7ef6, U+7efa-7efb, U+7efe, U+7f01-7f04, U+7f08, U+7f0a-7f12, U+7f17, U+7f19, U+7f1b-7f1c, U+7f1f,
|
U+7ef0-7ef2, U+7ef6, U+7efa-7efb, U+7efe, U+7f01-7f04, U+7f08, U+7f0a-7f12, U+7f17, U+7f19, U+7f1b-7f1c, U+7f1f,
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4af13061394e55b3.woff2) format('woff2');
|
src: url(./fonts/4af13061394e55b3.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7dd2, U+7dd4, U+7dd6-7dd8, U+7dda-7de0, U+7de2-7de6, U+7de8-7ded, U+7def, U+7df1-7df5, U+7df7, U+7df9,
|
U+7dd2, U+7dd4, U+7dd6-7dd8, U+7dda-7de0, U+7de2-7de6, U+7de8-7ded, U+7def, U+7df1-7df5, U+7df7, U+7df9,
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/a3739966a326e377.woff2) format('woff2');
|
src: url(./fonts/a3739966a326e377.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7ce8, U+7cec, U+7cf0, U+7cf5-7cf9, U+7cfc, U+7cfe, U+7d00, U+7d04-7d0b, U+7d0d, U+7d10-7d14, U+7d17-7d19,
|
U+7ce8, U+7cec, U+7cf0, U+7cf5-7cf9, U+7cfc, U+7cfe, U+7d00, U+7d04-7d0b, U+7d0d, U+7d10-7d14, U+7d17-7d19,
|
||||||
@@ -554,7 +554,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/6c7df51c2d3405f8.woff2) format('woff2');
|
src: url(./fonts/6c7df51c2d3405f8.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7bd3-7bd4, U+7bd9-7bda, U+7bdd, U+7be0-7be1, U+7be4-7be6, U+7be9-7bea, U+7bef, U+7bf4, U+7bf6, U+7bfc, U+7bfe,
|
U+7bd3-7bd4, U+7bd9-7bda, U+7bdd, U+7be0-7be1, U+7be4-7be6, U+7be9-7bea, U+7bef, U+7bf4, U+7bf6, U+7bfc, U+7bfe,
|
||||||
@@ -568,7 +568,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/a8901603a8f60b5c.woff2) format('woff2');
|
src: url(./fonts/a8901603a8f60b5c.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7ae6, U+7af4-7af7, U+7afa-7afb, U+7afd-7b0a, U+7b0c, U+7b0e-7b0f, U+7b13, U+7b15-7b16, U+7b18-7b19, U+7b1e-7b20,
|
U+7ae6, U+7af4-7af7, U+7afa-7afb, U+7afd-7b0a, U+7b0c, U+7b0e-7b0f, U+7b13, U+7b15-7b16, U+7b18-7b19, U+7b1e-7b20,
|
||||||
@@ -581,7 +581,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/62d567322492159f.woff2) format('woff2');
|
src: url(./fonts/62d567322492159f.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+798b-798e, U+7992, U+7994-7995, U+7997-7998, U+799a-799c, U+799f, U+79a3-79a6, U+79a8-79ac, U+79ae-79b1,
|
U+798b-798e, U+7992, U+7994-7995, U+7997-7998, U+799a-799c, U+799f, U+79a3-79a6, U+79a8-79ac, U+79ae-79b1,
|
||||||
@@ -597,7 +597,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4ca36ec0898d9f4b.woff2) format('woff2');
|
src: url(./fonts/4ca36ec0898d9f4b.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+784c, U+784e-7854, U+7856-7857, U+7859-785a, U+7865, U+7869-786a, U+786d, U+786f, U+7876-7877, U+787c,
|
U+784c, U+784e-7854, U+7856-7857, U+7859-785a, U+7865, U+7869-786a, U+786d, U+786f, U+7876-7877, U+787c,
|
||||||
@@ -613,7 +613,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/dc00089f866f9369.woff2) format('woff2');
|
src: url(./fonts/dc00089f866f9369.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7722, U+7726, U+7728, U+772b-7730, U+7732-7736, U+7739-773a, U+773d-773f, U+7743, U+7746-7747, U+774c-774f,
|
U+7722, U+7726, U+7728, U+772b-7730, U+7732-7736, U+7739-773a, U+773d-773f, U+7743, U+7746-7747, U+774c-774f,
|
||||||
@@ -628,7 +628,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b14a1ed0396f00cf.woff2) format('woff2');
|
src: url(./fonts/b14a1ed0396f00cf.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7613-7619, U+761b-761d, U+761f-7622, U+7625, U+7627-762a, U+762e-7630, U+7632-7635, U+7638-763a, U+763c-763d,
|
U+7613-7619, U+761b-761d, U+761f-7622, U+7625, U+7627-762a, U+762e-7630, U+7632-7635, U+7638-763a, U+763c-763d,
|
||||||
@@ -643,7 +643,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/beed123d60cc6155.woff2) format('woff2');
|
src: url(./fonts/beed123d60cc6155.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+750d, U+750f, U+7511, U+7513, U+7515, U+7517, U+7519, U+7521-7527, U+752a, U+752c-752d, U+752f, U+7534, U+7536,
|
U+750d, U+750f, U+7511, U+7513, U+7515, U+7517, U+7519, U+7521-7527, U+752a, U+752c-752d, U+752f, U+7534, U+7536,
|
||||||
@@ -658,7 +658,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/9fcc77355f5a0d52.woff2) format('woff2');
|
src: url(./fonts/9fcc77355f5a0d52.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+73f0, U+73f2, U+73f4-73f5, U+73f7, U+73f9-73fa, U+73fc-73fd, U+73ff-7402, U+7404, U+7407-7408, U+740a-740f,
|
U+73f0, U+73f2, U+73f4-73f5, U+73f7, U+73f9-73fa, U+73fc-73fd, U+73ff-7402, U+7404, U+7407-7408, U+740a-740f,
|
||||||
@@ -673,7 +673,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/500793f7237d7337.woff2) format('woff2');
|
src: url(./fonts/500793f7237d7337.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+72e6, U+72e8, U+72ef-72f0, U+72f2-72f4, U+72f6-72f7, U+72f9-72fb, U+72fd, U+7300-7304, U+7307, U+730a-730c,
|
U+72e6, U+72e8, U+72ef-72f0, U+72f2-72f4, U+72f6-72f7, U+72f9-72fb, U+72fd, U+7300-7304, U+7307, U+730a-730c,
|
||||||
@@ -688,7 +688,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/c4c44fadfc63ac54.woff2) format('woff2');
|
src: url(./fonts/c4c44fadfc63ac54.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+71a8, U+71af, U+71b1-71bc, U+71be, U+71c1-71c2, U+71c4, U+71c8-71cb, U+71ce-71d0, U+71d2, U+71d4, U+71d9-71da,
|
U+71a8, U+71af, U+71b1-71bc, U+71be, U+71c1-71c2, U+71c4, U+71c8-71cb, U+71ce-71d0, U+71d2, U+71d4, U+71d9-71da,
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/cf9cd646bc0d0f19.woff2) format('woff2');
|
src: url(./fonts/cf9cd646bc0d0f19.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+700b, U+700d, U+7015, U+7018, U+701b, U+701d-701f, U+7023, U+7026-7028, U+702c, U+702e-7030, U+7035, U+7037,
|
U+700b, U+700d, U+7015, U+7018, U+701b, U+701d-701f, U+7023, U+7026-7028, U+702c, U+702e-7030, U+7035, U+7037,
|
||||||
@@ -720,7 +720,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/620f5f28321be052.woff2) format('woff2');
|
src: url(./fonts/620f5f28321be052.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+6ed9, U+6edb, U+6edd, U+6edf-6ee0, U+6ee2, U+6ee6, U+6eea, U+6eec, U+6eee-6eef, U+6ef2-6ef3, U+6ef7-6efa, U+6efe,
|
U+6ed9, U+6edb, U+6edd, U+6edf-6ee0, U+6ee2, U+6ee6, U+6eea, U+6eec, U+6eee-6eef, U+6ef2-6ef3, U+6ef7-6efa, U+6efe,
|
||||||
@@ -735,7 +735,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/658210c7f683a557.woff2) format('woff2');
|
src: url(./fonts/658210c7f683a557.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+6dc3, U+6dc5-6dc6, U+6dc9, U+6dcc, U+6dcf, U+6dd2-6dd3, U+6dd6, U+6dd9-6dde, U+6de0, U+6de4, U+6de6, U+6de8-6dea,
|
U+6dc3, U+6dc5-6dc6, U+6dc9, U+6dcc, U+6dcf, U+6dd2-6dd3, U+6dd6, U+6dd9-6dde, U+6de0, U+6de4, U+6de6, U+6de8-6dea,
|
||||||
@@ -750,7 +750,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/795b87224e8dcbc4.woff2) format('woff2');
|
src: url(./fonts/795b87224e8dcbc4.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+6cb1-6cb2, U+6cb4-6cb5, U+6cb7, U+6cba, U+6cbc-6cbd, U+6cc1-6cc3, U+6cc5-6cc7, U+6cd0-6cd4, U+6cd6-6cd7,
|
U+6cb1-6cb2, U+6cb4-6cb5, U+6cb7, U+6cba, U+6cbc-6cbd, U+6cc1-6cc3, U+6cc5-6cc7, U+6cd0-6cd4, U+6cd6-6cd7,
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/e9d83a6fe76066e1.woff2) format('woff2');
|
src: url(./fonts/e9d83a6fe76066e1.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+6b83-6b86, U+6b89, U+6b8d, U+6b91-6b93, U+6b95, U+6b97-6b98, U+6b9a-6b9b, U+6b9e, U+6ba1-6ba4, U+6ba9-6baa,
|
U+6b83-6b86, U+6b89, U+6b8d, U+6b91-6b93, U+6b95, U+6b97-6b98, U+6b9a-6b9b, U+6b9e, U+6ba1-6ba4, U+6ba9-6baa,
|
||||||
@@ -780,7 +780,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/df78bcc0618fdcb2.woff2) format('woff2');
|
src: url(./fonts/df78bcc0618fdcb2.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+69fe-6a01, U+6a06, U+6a09, U+6a0b, U+6a11, U+6a13, U+6a17-6a19, U+6a1b, U+6a1e, U+6a23, U+6a28-6a29, U+6a2b,
|
U+69fe-6a01, U+6a06, U+6a09, U+6a0b, U+6a11, U+6a13, U+6a17-6a19, U+6a1b, U+6a1e, U+6a23, U+6a28-6a29, U+6a2b,
|
||||||
@@ -796,7 +796,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/cf0b566ed87b1702.woff2) format('woff2');
|
src: url(./fonts/cf0b566ed87b1702.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+68d3, U+68d7, U+68dd, U+68df, U+68e1, U+68e3-68e4, U+68e6-68ed, U+68ef-68f0, U+68f2, U+68f4, U+68f6-68f7, U+68f9,
|
U+68d3, U+68d7, U+68dd, U+68df, U+68e1, U+68e3-68e4, U+68e6-68ed, U+68ef-68f0, U+68f2, U+68f4, U+68f6-68f7, U+68f9,
|
||||||
@@ -811,7 +811,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b4bf867aff4a6bde.woff2) format('woff2');
|
src: url(./fonts/b4bf867aff4a6bde.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+678b-678d, U+678f, U+6792-6793, U+6796, U+6798, U+679e-67a1, U+67a5, U+67a7-67a9, U+67ac-67ad, U+67b0-67b1,
|
U+678b-678d, U+678f, U+6792-6793, U+6796, U+6798, U+679e-67a1, U+67a5, U+67a7-67a9, U+67ac-67ad, U+67b0-67b1,
|
||||||
@@ -827,7 +827,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/23f09a3fd29c803a.woff2) format('woff2');
|
src: url(./fonts/23f09a3fd29c803a.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+6631, U+6633-6634, U+6636, U+663a-663b, U+663d, U+6641, U+6644-6645, U+6649, U+664c, U+664f, U+6654, U+6659,
|
U+6631, U+6633-6634, U+6636, U+663a-663b, U+663d, U+6641, U+6644-6645, U+6649, U+664c, U+664f, U+6654, U+6659,
|
||||||
@@ -843,7 +843,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/e110453047bfe034.woff2) format('woff2');
|
src: url(./fonts/e110453047bfe034.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+64f1-64f2, U+64f4, U+64f7-64f8, U+64fa, U+64fc, U+64fe-64ff, U+6503, U+6509, U+650f, U+6514, U+6518, U+651c-651e,
|
U+64f1-64f2, U+64f4, U+64f7-64f8, U+64fa, U+64fc, U+64fe-64ff, U+6503, U+6509, U+650f, U+6514, U+6518, U+651c-651e,
|
||||||
@@ -859,7 +859,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b7668e18fe9cce6c.woff2) format('woff2');
|
src: url(./fonts/b7668e18fe9cce6c.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+63b8-63bc, U+63be, U+63c0, U+63c3-63c4, U+63c6, U+63c8, U+63cd-63ce, U+63d1, U+63d6, U+63da-63db, U+63de, U+63e0,
|
U+63b8-63bc, U+63be, U+63c0, U+63c3-63c4, U+63c6, U+63c8, U+63cd-63ce, U+63d1, U+63d6, U+63da-63db, U+63de, U+63e0,
|
||||||
@@ -874,7 +874,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/2306716a58a58a05.woff2) format('woff2');
|
src: url(./fonts/2306716a58a58a05.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+622c, U+622e-6230, U+6232, U+6238, U+623b, U+623d-623e, U+6243, U+6246, U+6248-6249, U+624c, U+6255, U+6259,
|
U+622c, U+622e-6230, U+6232, U+6238, U+623b, U+623d-623e, U+6243, U+6246, U+6248-6249, U+624c, U+6255, U+6259,
|
||||||
@@ -890,7 +890,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b37a954f47c08e8e.woff2) format('woff2');
|
src: url(./fonts/b37a954f47c08e8e.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+60ed-60ee, U+60f0-60f1, U+60f4, U+60f6, U+60fa, U+6100, U+6106, U+610d-610e, U+6112, U+6114-6115, U+6119, U+611c,
|
U+60ed-60ee, U+60f0-60f1, U+60f4, U+60f6, U+60fa, U+6100, U+6106, U+610d-610e, U+6112, U+6114-6115, U+6119, U+611c,
|
||||||
@@ -905,7 +905,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/41d1d19f7bebcf26.woff2) format('woff2');
|
src: url(./fonts/41d1d19f7bebcf26.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5fc4, U+5fc9, U+5fcb, U+5fce-5fd6, U+5fda-5fde, U+5fe1-5fe2, U+5fe4-5fe5, U+5fea, U+5fed-5fee, U+5ff1-5ff3,
|
U+5fc4, U+5fc9, U+5fcb, U+5fce-5fd6, U+5fda-5fde, U+5fe1-5fe2, U+5fe4-5fe5, U+5fea, U+5fed-5fee, U+5ff1-5ff3,
|
||||||
@@ -920,7 +920,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/3c66c61b08f9dc28.woff2) format('woff2');
|
src: url(./fonts/3c66c61b08f9dc28.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5e98, U+5e9b, U+5e9d, U+5ea0-5ea5, U+5ea8, U+5eab, U+5eaf, U+5eb3, U+5eb5-5eb6, U+5eb9, U+5ebe, U+5ec1-5ec3,
|
U+5e98, U+5e9b, U+5e9d, U+5ea0-5ea5, U+5ea8, U+5eab, U+5eaf, U+5eb3, U+5eb5-5eb6, U+5eb9, U+5ebe, U+5ec1-5ec3,
|
||||||
@@ -935,7 +935,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/8a51ce32a2d6a2b0.woff2) format('woff2');
|
src: url(./fonts/8a51ce32a2d6a2b0.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5d26-5d27, U+5d2e-5d34, U+5d3c-5d3e, U+5d41-5d44, U+5d46-5d48, U+5d4a-5d4b, U+5d4e, U+5d50, U+5d52, U+5d55-5d58,
|
U+5d26-5d27, U+5d2e-5d34, U+5d3c-5d3e, U+5d41-5d44, U+5d46-5d48, U+5d4a-5d4b, U+5d4e, U+5d50, U+5d52, U+5d55-5d58,
|
||||||
@@ -950,7 +950,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/ffa6ae4b9eb5ffdc.woff2) format('woff2');
|
src: url(./fonts/ffa6ae4b9eb5ffdc.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5bec, U+5bee-5bf0, U+5bf2-5bf3, U+5bf5-5bf6, U+5bfe, U+5c02-5c03, U+5c05, U+5c07-5c09, U+5c0b-5c0c, U+5c0e,
|
U+5bec, U+5bee-5bf0, U+5bf2-5bf3, U+5bf5-5bf6, U+5bfe, U+5c02-5c03, U+5c05, U+5c07-5c09, U+5c0b-5c0c, U+5c0e,
|
||||||
@@ -965,7 +965,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/96088f438d29f54b.woff2) format('woff2');
|
src: url(./fonts/96088f438d29f54b.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5aa0, U+5aa3-5aa4, U+5aaa, U+5aae-5aaf, U+5ab1-5ab2, U+5ab4-5ab5, U+5ab7-5aba, U+5abd-5abf, U+5ac3-5ac4,
|
U+5aa0, U+5aa3-5aa4, U+5aaa, U+5aae-5aaf, U+5ab1-5ab2, U+5ab4-5ab5, U+5ab7-5aba, U+5abd-5abf, U+5ac3-5ac4,
|
||||||
@@ -980,7 +980,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/e0d8fdb665a16cd2.woff2) format('woff2');
|
src: url(./fonts/e0d8fdb665a16cd2.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+596a, U+596c-596e, U+5977, U+597b-597c, U+5981, U+598f, U+5997-5998, U+599a, U+599c-599d, U+59a0-59a1,
|
U+596a, U+596c-596e, U+5977, U+597b-597c, U+5981, U+598f, U+5997-5998, U+599a, U+599c-599d, U+59a0-59a1,
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/20f6ba8c0c269b1a.woff2) format('woff2');
|
src: url(./fonts/20f6ba8c0c269b1a.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5820, U+5822-5823, U+5825-5826, U+582c, U+582f, U+5831, U+583a, U+583d, U+583f-5842, U+5844-5846, U+5848, U+584a,
|
U+5820, U+5822-5823, U+5825-5826, U+582c, U+582f, U+5831, U+583a, U+583d, U+583f-5842, U+5844-5846, U+5848, U+584a,
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/847818b9c19663fd.woff2) format('woff2');
|
src: url(./fonts/847818b9c19663fd.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+56f9, U+56fc, U+56ff-5700, U+5703-5704, U+5709-570a, U+570c-570d, U+570f, U+5712-5713, U+5718-5719, U+571c,
|
U+56f9, U+56fc, U+56ff-5700, U+5703-5704, U+5709-570a, U+570c-570d, U+570f, U+5712-5713, U+5718-5719, U+571c,
|
||||||
@@ -1026,7 +1026,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/a7c81fbdd8cc5eb5.woff2) format('woff2');
|
src: url(./fonts/a7c81fbdd8cc5eb5.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+55f5-55f7, U+55fb, U+55fe, U+5600-5601, U+5605-5606, U+5608, U+560c-560d, U+560f, U+5614, U+5616-5617, U+561a,
|
U+55f5-55f7, U+55fb, U+55fe, U+5600-5601, U+5605-5606, U+5608, U+560c-560d, U+560f, U+5614, U+5616-5617, U+561a,
|
||||||
@@ -1040,7 +1040,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/23acd90334da40b5.woff2) format('woff2');
|
src: url(./fonts/23acd90334da40b5.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+550f, U+5511-5514, U+5516-5517, U+5519, U+551b, U+551d-551e, U+5520, U+5522-5523, U+5526-5527, U+552a-552c,
|
U+550f, U+5511-5514, U+5516-5517, U+5519, U+551b, U+551d-551e, U+5520, U+5522-5523, U+5526-5527, U+552a-552c,
|
||||||
@@ -1054,7 +1054,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/aebf42c37a635878.woff2) format('woff2');
|
src: url(./fonts/aebf42c37a635878.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+53e7-53e9, U+53f1, U+53f4-53f5, U+53fa-5400, U+5402, U+5405-5407, U+540b, U+540f, U+5412, U+5414, U+5416,
|
U+53e7-53e9, U+53f1, U+53f4-53f5, U+53fa-5400, U+5402, U+5405-5407, U+540b, U+540f, U+5412, U+5414, U+5416,
|
||||||
@@ -1069,7 +1069,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/c08b4b4d95cfb2ea.woff2) format('woff2');
|
src: url(./fonts/c08b4b4d95cfb2ea.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5289, U+528b, U+528d, U+528f, U+5291-5293, U+529a, U+52a2, U+52a6-52a7, U+52ac-52ad, U+52af, U+52b4-52b5, U+52b9,
|
U+5289, U+528b, U+528d, U+528f, U+5291-5293, U+529a, U+52a2, U+52a6-52a7, U+52ac-52ad, U+52af, U+52b4-52b5, U+52b9,
|
||||||
@@ -1085,7 +1085,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/5b4fc0b27a8a1464.woff2) format('woff2');
|
src: url(./fonts/5b4fc0b27a8a1464.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5104, U+5106-5107, U+5109-510b, U+510d, U+510f-5110, U+5113, U+5115, U+5117-5118, U+511a-511c, U+511e-511f,
|
U+5104, U+5106-5107, U+5109-510b, U+510d, U+510f-5110, U+5113, U+5115, U+5117-5118, U+511a-511c, U+511e-511f,
|
||||||
@@ -1101,7 +1101,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/14213d7fbd6ab158.woff2) format('woff2');
|
src: url(./fonts/14213d7fbd6ab158.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+4fd1, U+4fd3, U+4fda-4fdc, U+4fdf-4fe0, U+4fe2-4fe4, U+4fe6, U+4fe8, U+4feb-4fed, U+4ff3, U+4ff5-4ff6, U+4ff8,
|
U+4fd1, U+4fd3, U+4fda-4fdc, U+4fdf-4fe0, U+4fe2-4fe4, U+4fe6, U+4fe8, U+4feb-4fed, U+4ff3, U+4ff5-4ff6, U+4ff8,
|
||||||
@@ -1116,7 +1116,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/3ff1abbbffc86b31.woff2) format('woff2');
|
src: url(./fonts/3ff1abbbffc86b31.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+4ea3, U+4ea5, U+4eb0-4eb1, U+4eb3-4eb6, U+4eb8-4eb9, U+4ebb-4ebe, U+4ec2-4ec4, U+4ec8-4ec9, U+4ecc, U+4ecf-4ed0,
|
U+4ea3, U+4ea5, U+4eb0-4eb1, U+4eb3-4eb6, U+4eb8-4eb9, U+4ebb-4ebe, U+4ec2-4ec4, U+4ec8-4ec9, U+4ecc, U+4ecf-4ed0,
|
||||||
@@ -1131,7 +1131,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/5e6011e3010d50bd.woff2) format('woff2');
|
src: url(./fonts/5e6011e3010d50bd.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+3129, U+3131, U+3134, U+3137, U+3139, U+3141-3142, U+3145, U+3147-3148, U+314b, U+314d-314e, U+315c, U+3160-3161,
|
U+3129, U+3131, U+3134, U+3137, U+3139, U+3141-3142, U+3145, U+3147-3148, U+314b, U+314d-314e, U+315c, U+3160-3161,
|
||||||
@@ -1147,7 +1147,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/65a11622c84af2c7.woff2) format('woff2');
|
src: url(./fonts/65a11622c84af2c7.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+279f-27a2, U+27a4-27a5, U+27a8, U+27b0, U+27b2-27b3, U+27b9, U+27e8-27e9, U+27f6, U+2800, U+28ec, U+2913,
|
U+279f-27a2, U+27a4-27a5, U+27a8, U+27b0, U+27b2-27b3, U+27b9, U+27e8-27e9, U+27f6, U+2800, U+28ec, U+2913,
|
||||||
@@ -1162,7 +1162,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/6d4e9520e29f6d02.woff2) format('woff2');
|
src: url(./fonts/6d4e9520e29f6d02.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+2651-2655, U+2658, U+265a-265b, U+265d-265e, U+2660-266d, U+266f, U+267b, U+2688, U+2693-2696, U+2698-2699,
|
U+2651-2655, U+2658, U+265a-265b, U+265d-265e, U+2660-266d, U+266f, U+267b, U+2688, U+2693-2696, U+2698-2699,
|
||||||
@@ -1175,7 +1175,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/497141c2daba02b0.woff2) format('woff2');
|
src: url(./fonts/497141c2daba02b0.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+2550-2551, U+2554, U+2557, U+255a-255b, U+255d, U+255f-2560, U+2562-2563, U+2565-2567, U+2569-256a, U+256c-2572,
|
U+2550-2551, U+2554, U+2557, U+255a-255b, U+255d, U+255f-2560, U+2562-2563, U+2565-2567, U+2569-256a, U+256c-2572,
|
||||||
@@ -1187,7 +1187,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/eb4969d421026bd0.woff2) format('woff2');
|
src: url(./fonts/eb4969d421026bd0.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+23f0, U+23f3, U+2445, U+2449, U+2465-2471, U+2474-249b, U+24b8, U+24c2, U+24c7, U+24c9, U+24d0, U+24d2, U+24d4,
|
U+23f0, U+23f3, U+2445, U+2449, U+2465-2471, U+2474-249b, U+24b8, U+24c2, U+24c7, U+24c9, U+24d0, U+24d2, U+24d4,
|
||||||
@@ -1198,7 +1198,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/48c235f25cd265a1.woff2) format('woff2');
|
src: url(./fonts/48c235f25cd265a1.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+207c-2083, U+208c-208e, U+2092, U+20a6, U+20a8-20ad, U+20af, U+20b1, U+20b4-20b5, U+20b8-20ba, U+20bd, U+20db,
|
U+207c-2083, U+208c-208e, U+2092, U+20a6, U+20a8-20ad, U+20af, U+20b1, U+20b4-20b5, U+20b8-20ba, U+20bd, U+20db,
|
||||||
@@ -1213,7 +1213,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/0a8cf734a0b6b153.woff2) format('woff2');
|
src: url(./fonts/0a8cf734a0b6b153.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+1d34-1d35, U+1d38-1d3a, U+1d3c, U+1d3f-1d40, U+1d49, U+1d4e-1d4f, U+1d52, U+1d55, U+1d5b, U+1d5e, U+1d9c, U+1da0,
|
U+1d34-1d35, U+1d38-1d3a, U+1d3c, U+1d3f-1d40, U+1d49, U+1d4e-1d4f, U+1d52, U+1d55, U+1d5b, U+1d5e, U+1d9c, U+1da0,
|
||||||
@@ -1227,7 +1227,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b42723fe53599b90.woff2) format('woff2');
|
src: url(./fonts/b42723fe53599b90.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+2ae-2b3, U+2b5-2bf, U+2c2-2c3, U+2c6-2d1, U+2d8-2da, U+2dc, U+2e1-2e3, U+2e5, U+2eb, U+2ee-2f0, U+2f2-2f7,
|
U+2ae-2b3, U+2b5-2bf, U+2c2-2c3, U+2c6-2d1, U+2d8-2da, U+2dc, U+2e1-2e3, U+2e5, U+2eb, U+2ee-2f0, U+2f2-2f7,
|
||||||
@@ -1239,7 +1239,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/05a7d600a249b837.woff2) format('woff2');
|
src: url(./fonts/05a7d600a249b837.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+176-17f, U+192, U+194, U+19a-19b, U+19d, U+1a0-1a1, U+1a3-1a4, U+1aa, U+1ac-1ad, U+1af-1bf, U+1d2, U+1d4, U+1d6,
|
U+176-17f, U+192, U+194, U+19a-19b, U+19d, U+1a0-1a1, U+1a3-1a4, U+1aa, U+1ac-1ad, U+1af-1bf, U+1d2, U+1d4, U+1d6,
|
||||||
@@ -1252,7 +1252,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b73283458ef26d55.woff2) format('woff2');
|
src: url(./fonts/b73283458ef26d55.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+a1-a4, U+a6-a8, U+aa, U+ac, U+af, U+b1, U+b3-b6, U+b8-ba, U+bc-d6, U+d8-de, U+e6, U+eb, U+ee-f0, U+f5, U+f7-f8,
|
U+a1-a4, U+a6-a8, U+aa, U+ac, U+af, U+b1, U+b3-b6, U+b8-ba, U+bc-d6, U+d8-de, U+e6, U+eb, U+ee-f0, U+f5, U+f7-f8,
|
||||||
@@ -1264,7 +1264,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/2944f60aea487c2a.woff2) format('woff2');
|
src: url(./fonts/2944f60aea487c2a.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+221a, U+2264, U+2464, U+25a0, U+3008, U+4e10, U+512a, U+5152, U+5201, U+5241, U+5340, U+5352, U+549a, U+54b2,
|
U+221a, U+2264, U+2464, U+25a0, U+3008, U+4e10, U+512a, U+5152, U+5201, U+5241, U+5340, U+5352, U+549a, U+54b2,
|
||||||
@@ -1286,7 +1286,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/b140f9d60ba877f5.woff2) format('woff2');
|
src: url(./fonts/b140f9d60ba877f5.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+161, U+926, U+928, U+939, U+93f-940, U+94d, U+e17, U+e22, U+e44, U+25c7, U+25ce, U+2764, U+3009, U+3016-3017,
|
U+161, U+926, U+928, U+939, U+93f-940, U+94d, U+e17, U+e22, U+e44, U+25c7, U+25ce, U+2764, U+3009, U+3016-3017,
|
||||||
@@ -1307,7 +1307,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/3bf9956fe5c594f4.woff2) format('woff2');
|
src: url(./fonts/3bf9956fe5c594f4.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+10c, U+627-629, U+639, U+644, U+64a, U+203b, U+2265, U+2463, U+2573, U+25b2, U+3448-3449, U+4e1e, U+4e5e, U+4f3a,
|
U+10c, U+627-629, U+639, U+644, U+64a, U+203b, U+2265, U+2463, U+2573, U+25b2, U+3448-3449, U+4e1e, U+4e5e, U+4f3a,
|
||||||
@@ -1329,7 +1329,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/47d3dc9f8403f684.woff2) format('woff2');
|
src: url(./fonts/47d3dc9f8403f684.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+60, U+631, U+2606, U+3014-3015, U+309c, U+33a1, U+4e52, U+4ec6, U+4f86, U+4f8d, U+4fde, U+4fef, U+500b, U+502a,
|
U+60, U+631, U+2606, U+3014-3015, U+309c, U+33a1, U+4e52, U+4ec6, U+4f86, U+4f8d, U+4fde, U+4fef, U+500b, U+502a,
|
||||||
@@ -1351,7 +1351,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/4a7ea73dc15f2b63.woff2) format('woff2');
|
src: url(./fonts/4a7ea73dc15f2b63.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+2003, U+2193, U+2462, U+4e19, U+4e2b, U+4e36, U+4ea8, U+4ed1, U+4ed7, U+4f51, U+4f63, U+4f83, U+50e7, U+5112,
|
U+2003, U+2193, U+2462, U+4e19, U+4e2b, U+4e36, U+4ea8, U+4ed1, U+4ed7, U+4f51, U+4f63, U+4f83, U+50e7, U+5112,
|
||||||
@@ -1373,7 +1373,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f0721a3949f84d42.woff2) format('woff2');
|
src: url(./fonts/f0721a3949f84d42.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+5e, U+2190, U+250a, U+25bc, U+25cf, U+4e56, U+4ea9, U+4f3d, U+4f6c, U+4f88, U+4fa8, U+4fcf, U+5029, U+5188,
|
U+5e, U+2190, U+250a, U+25bc, U+25cf, U+4e56, U+4ea9, U+4f3d, U+4f6c, U+4f88, U+4fa8, U+4fcf, U+5029, U+5188,
|
||||||
@@ -1395,7 +1395,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/71139ee70c1b1fce.woff2) format('woff2');
|
src: url(./fonts/71139ee70c1b1fce.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+b0, U+2191, U+2460-2461, U+25c6, U+300e-300f, U+4e1b, U+4e7e, U+4ed5, U+4ef2, U+4f10, U+4f1e, U+4f50, U+4fa6,
|
U+b0, U+2191, U+2460-2461, U+25c6, U+300e-300f, U+4e1b, U+4e7e, U+4ed5, U+4ef2, U+4f10, U+4f1e, U+4f50, U+4fa6,
|
||||||
@@ -1417,7 +1417,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/75c0090760a68ac5.woff2) format('woff2');
|
src: url(./fonts/75c0090760a68ac5.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+200b, U+2103, U+4e18, U+4e27-4e28, U+4e38, U+4e59, U+4e8f, U+4ead, U+4ec7, U+4fe9, U+503a, U+5085, U+5146, U+51af,
|
U+200b, U+2103, U+4e18, U+4e27-4e28, U+4e38, U+4e59, U+4e8f, U+4ead, U+4ec7, U+4fe9, U+503a, U+5085, U+5146, U+51af,
|
||||||
@@ -1439,7 +1439,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/8eae1f2d9b73ca52.woff2) format('woff2');
|
src: url(./fonts/8eae1f2d9b73ca52.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+24, U+4e08, U+4e43, U+4e4f, U+4ef0, U+4f2a, U+507f, U+50ac, U+50bb, U+5151, U+51bb, U+51f6, U+51fd, U+5272,
|
U+24, U+4e08, U+4e43, U+4e4f, U+4ef0, U+4f2a, U+507f, U+50ac, U+50bb, U+5151, U+51bb, U+51f6, U+51fd, U+5272,
|
||||||
@@ -1461,7 +1461,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/a263283eb6ff7679.woff2) format('woff2');
|
src: url(./fonts/a263283eb6ff7679.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+a5, U+2192, U+2605, U+4e11, U+4e22, U+4e32, U+4f0d, U+4f0f, U+4f69, U+4ff1, U+50b2, U+5154, U+51dd, U+51f0,
|
U+a5, U+2192, U+2605, U+4e11, U+4e22, U+4e32, U+4f0d, U+4f0f, U+4f69, U+4ff1, U+50b2, U+5154, U+51dd, U+51f0,
|
||||||
@@ -1483,7 +1483,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/09c325c9da420816.woff2) format('woff2');
|
src: url(./fonts/09c325c9da420816.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+300c-300d, U+4e54, U+4e58, U+4e95, U+4ec1, U+4f2f, U+4f38, U+4fa3, U+4fca, U+503e, U+5141, U+5144, U+517c, U+51cc,
|
U+300c-300d, U+4e54, U+4e58, U+4e95, U+4ec1, U+4f2f, U+4f38, U+4fa3, U+4fca, U+503e, U+5141, U+5144, U+517c, U+51cc,
|
||||||
@@ -1505,7 +1505,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/053609e6b57c594f.woff2) format('woff2');
|
src: url(./fonts/053609e6b57c594f.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+4e4c, U+4e88, U+4ea1, U+4ea6, U+4ed3-4ed4, U+4eff, U+4f30, U+4fa7, U+4fc4, U+4fd7, U+500d, U+504f, U+5076-5077,
|
U+4e4c, U+4e88, U+4ea1, U+4ea6, U+4ed3-4ed4, U+4eff, U+4f30, U+4fa7, U+4fc4, U+4fd7, U+500d, U+504f, U+5076-5077,
|
||||||
@@ -1527,7 +1527,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/5c92b2016607caeb.woff2) format('woff2');
|
src: url(./fonts/5c92b2016607caeb.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+23, U+3d, U+4e01, U+4e39, U+4e73, U+4ecd, U+4ed9, U+4eea, U+4f0a, U+4f1f, U+4f5b, U+4fa0, U+4fc3, U+501f, U+50a8,
|
U+23, U+3d, U+4e01, U+4e39, U+4e73, U+4ecd, U+4ed9, U+4eea, U+4f0a, U+4f1f, U+4f5b, U+4fa0, U+4fc3, U+501f, U+50a8,
|
||||||
@@ -1548,7 +1548,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f192aec3fa1f17f4.woff2) format('woff2');
|
src: url(./fonts/f192aec3fa1f17f4.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+26, U+3c, U+d7, U+4e4e, U+4e61, U+4e71, U+4ebf, U+4f26, U+5012, U+51ac, U+51b0, U+51b2, U+51b7, U+5218, U+521a,
|
U+26, U+3c, U+d7, U+4e4e, U+4e61, U+4e71, U+4ebf, U+4f26, U+5012, U+51ac, U+51b0, U+51b2, U+51b7, U+5218, U+521a,
|
||||||
@@ -1569,7 +1569,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f6255320a8ecc997.woff2) format('woff2');
|
src: url(./fonts/f6255320a8ecc997.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+7e, U+2026, U+4e03, U+4e25, U+4e30, U+4e34, U+4e45, U+4e5d, U+4e89, U+4eae, U+4ed8, U+4f11, U+4f19, U+4f24,
|
U+7e, U+2026, U+4e03, U+4e25, U+4e30, U+4e34, U+4e45, U+4e5d, U+4e89, U+4eae, U+4ed8, U+4f11, U+4f19, U+4f24,
|
||||||
@@ -1591,7 +1591,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/5849283072442ca2.woff2) format('woff2');
|
src: url(./fonts/5849283072442ca2.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+25, U+4e14, U+4e1d, U+4e3d, U+4e49, U+4e60, U+4e9a, U+4eb2, U+4ec5, U+4efd, U+4f3c, U+4f4f, U+4f8b, U+4fbf,
|
U+25, U+4e14, U+4e1d, U+4e3d, U+4e49, U+4e60, U+4e9a, U+4eb2, U+4ec5, U+4efd, U+4f3c, U+4f4f, U+4f8b, U+4fbf,
|
||||||
@@ -1613,7 +1613,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/04eaf1a999368419.woff2) format('woff2');
|
src: url(./fonts/04eaf1a999368419.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+2b, U+40, U+3000, U+300a-300b, U+4e16, U+4e66, U+4e70, U+4e91-4e92, U+4e94, U+4e9b, U+4ec0, U+4eca, U+4f01,
|
U+2b, U+40, U+3000, U+300a-300b, U+4e16, U+4e66, U+4e70, U+4e91-4e92, U+4e94, U+4e9b, U+4ec0, U+4eca, U+4f01,
|
||||||
@@ -1635,7 +1635,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/2ef4d8c6c25aaa84.woff2) format('woff2');
|
src: url(./fonts/2ef4d8c6c25aaa84.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+4e, U+201c-201d, U+3010-3011, U+4e07, U+4e1c, U+4e24, U+4e3e, U+4e48, U+4e50, U+4e5f, U+4e8b-4e8c, U+4ea4,
|
U+4e, U+201c-201d, U+3010-3011, U+4e07, U+4e1c, U+4e24, U+4e3e, U+4e48, U+4e50, U+4e5f, U+4e8b-4e8c, U+4ea4,
|
||||||
@@ -1657,7 +1657,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/f63d970a7b2b5321.woff2) format('woff2');
|
src: url(./fonts/f63d970a7b2b5321.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+d, U+3e, U+5f, U+7c, U+a0, U+a9, U+4e09-4e0b, U+4e0d-4e0e, U+4e13, U+4e1a, U+4e2a, U+4e3a-4e3b, U+4e4b, U+4e86,
|
U+d, U+3e, U+5f, U+7c, U+a0, U+a9, U+4e09-4e0b, U+4e0d-4e0e, U+4e13, U+4e1a, U+4e2a, U+4e3a-4e3b, U+4e4b, U+4e86,
|
||||||
@@ -1678,7 +1678,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/ca6307f70603bdbf.woff2) format('woff2');
|
src: url(./fonts/ca6307f70603bdbf.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+20-22, U+27-2a, U+2c-3b, U+3f, U+41-4d, U+4f-5d, U+61-7b, U+7d, U+ab, U+ae, U+b2, U+b7, U+bb, U+df-e5, U+e7-ea,
|
U+20-22, U+27-2a, U+2c-3b, U+3f, U+41-4d, U+4f-5d, U+61-7b, U+7d, U+ab, U+ae, U+b2, U+b7, U+bb, U+df-e5, U+e7-ea,
|
||||||
@@ -1694,7 +1694,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/89c8b72714b2fb05.woff2) format('woff2');
|
src: url(./fonts/89c8b72714b2fb05.woff2) format('woff2');
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
@@ -1703,7 +1703,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/38475678e2d17cfd.woff2) format('woff2');
|
src: url(./fonts/38475678e2d17cfd.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
||||||
@@ -1714,7 +1714,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/59aba4ce37e0d3de.woff2) format('woff2');
|
src: url(./fonts/59aba4ce37e0d3de.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||||
@@ -1725,7 +1725,7 @@
|
|||||||
font-family: 'Noto Serif SC';
|
font-family: 'Noto Serif SC';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400 700;
|
font-weight: 400 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(./fonts/d8c61f0c834f348a.woff2) format('woff2');
|
src: url(./fonts/d8c61f0c834f348a.woff2) format('woff2');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function applyFontCatalog(fonts: CustomFont[]) {
|
|||||||
style = document.createElement('style'); style.id = 'rm-custom-fonts'; document.head.append(style)
|
style = document.createElement('style'); style.id = 'rm-custom-fonts'; document.head.append(style)
|
||||||
}
|
}
|
||||||
// Names are display-only; CSS identifiers and resource paths use only server-generated content hashes.
|
// Names are display-only; CSS identifiers and resource paths use only server-generated content hashes.
|
||||||
const css = fonts.map(font => `@font-face { font-family: '${font.id}'; src: url('${fontResourceRoot()}${font.id}/file'); font-display: swap; }`).join('\n')
|
const css = fonts.map(font => `@font-face { font-family: '${font.id}'; src: url('${fontResourceRoot()}${font.id}/file'); font-display: block; }`).join('\n')
|
||||||
if (style.textContent !== css) style.textContent = css
|
if (style.textContent !== css) style.textContent = css
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
/* Maple Mono v7.9; generated by scripts/prepare-maple-font.py. See fonts/maple/LICENSE.txt for licensing. */
|
/* Maple Mono v7.9; generated by scripts/prepare-maple-font.py. See fonts/maple/LICENSE.txt for licensing. */
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/maple/regular-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/maple/bold-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 700; font-display: block; src: url('./fonts/maple/bold-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: swap; src: url('./fonts/maple/italic-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 400; font-display: block; src: url('./fonts/maple/italic-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-03.woff2') format('woff2'); unicode-range: U+3000-3FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-04.woff2') format('woff2'); unicode-range: U+4000-4FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-05.woff2') format('woff2'); unicode-range: U+5000-5FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-06.woff2') format('woff2'); unicode-range: U+6000-6FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-07.woff2') format('woff2'); unicode-range: U+7000-7FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-08.woff2') format('woff2'); unicode-range: U+8000-8FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-09.woff2') format('woff2'); unicode-range: U+9000-9FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-0e.woff2') format('woff2'); unicode-range: U+E000-EFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-0f.woff2') format('woff2'); unicode-range: U+F000-FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-1f.woff2') format('woff2'); unicode-range: U+1F000-1FFFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-f0.woff2') format('woff2'); unicode-range: U+F0000-F0FFF; }
|
||||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: swap; src: url('./fonts/maple/bolditalic-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: italic; font-weight: 700; font-display: block; src: url('./fonts/maple/bolditalic-f1.woff2') format('woff2'); unicode-range: U+F1000-F1FFF; }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"demo_notice": "Editor preview only. Changes are not saved; refreshing restores the sample.",
|
"demo_notice": "Editor preview only. Changes are not saved; refreshing restores the sample.",
|
||||||
"retry_b8784c": "Retry",
|
"retry_b8784c": "Retry",
|
||||||
"fonts_could_not_be_loaded_42a574": "Some fonts could not be loaded. Using fallback fonts; refresh to try again.",
|
"fonts_could_not_be_loaded_42a574": "Fonts could not be loaded. Check your connection and refresh the page.",
|
||||||
"refresh_page_9366e1": "Refresh page",
|
"refresh_page_9366e1": "Refresh page",
|
||||||
"invalid_font_catalog_b577df": "Invalid font catalog",
|
"invalid_font_catalog_b577df": "Invalid font catalog",
|
||||||
"manrope_sans_serif_a8923c": "Manrope · Sans serif",
|
"manrope_sans_serif_a8923c": "Manrope · Sans serif",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"demo_notice": "仅供编辑器预览。修改不会保存,刷新后恢复示例。",
|
"demo_notice": "仅供编辑器预览。修改不会保存,刷新后恢复示例。",
|
||||||
"retry_b8784c": "重试",
|
"retry_b8784c": "重试",
|
||||||
"fonts_could_not_be_loaded_42a574": "部分字体加载失败,正在使用备用字体,可刷新页面重试。",
|
"fonts_could_not_be_loaded_42a574": "字体加载失败,请检查网络后刷新页面。",
|
||||||
"refresh_page_9366e1": "刷新页面",
|
"refresh_page_9366e1": "刷新页面",
|
||||||
"invalid_font_catalog_b577df": "字体列表格式无效",
|
"invalid_font_catalog_b577df": "字体列表格式无效",
|
||||||
"manrope_sans_serif_a8923c": "Manrope · 清晰无衬线",
|
"manrope_sans_serif_a8923c": "Manrope · 清晰无衬线",
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ import '@@/assets/styles/app.css'
|
|||||||
import '@@/assets/styles/element-plus.scss'
|
import '@@/assets/styles/element-plus.scss'
|
||||||
import 'virtual:uno.css'
|
import 'virtual:uno.css'
|
||||||
|
|
||||||
|
watchFontLoading()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(i18n).use(pinia).use(router)
|
app.use(i18n).use(pinia).use(router)
|
||||||
const user = useUserStore(pinia)
|
const user = useUserStore(pinia)
|
||||||
@@ -27,5 +28,5 @@ watch(() => user.sessionVersion, () => {
|
|||||||
})
|
})
|
||||||
// Mount a retryable error interface even if identity restoration temporarily fails.
|
// Mount a retryable error interface even if identity restoration temporarily fails.
|
||||||
void router.isReady().catch(() => {}).finally(() => {
|
void router.isReady().catch(() => {}).finally(() => {
|
||||||
app.mount('#app'); watchFontLoading(); void registerPwa()
|
app.mount('#app'); void registerPwa()
|
||||||
})
|
})
|
||||||
@@ -74,9 +74,6 @@ watch(() => [ws.active, tab.value?.status, ws.view, ws.images], () => {
|
|||||||
<div v-if="!tab" class="rm-empty">
|
<div v-if="!tab" class="rm-empty">
|
||||||
<p>{{ translate('workspace.select_or_create_a_note_669f82') }}</p>
|
<p>{{ translate('workspace.select_or_create_a_note_669f82') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.status === 'loading'" class="rm-empty" role="status">
|
|
||||||
<p>{{ translate('workspace.loading_4927a5') }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tab.status === 'error'" class="rm-empty">
|
<div v-else-if="tab.status === 'error'" class="rm-empty">
|
||||||
<p>{{ translate('workspace.could_not_open_this_note_634236') }}</p>
|
<p>{{ translate('workspace.could_not_open_this_note_634236') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { t as translate } from '@@/i18n/runtime'
|
|||||||
import { commandTitle } from '@@/utils/commands'
|
import { commandTitle } from '@@/utils/commands'
|
||||||
import { noteTitle } from '@@/utils/paths'
|
import { noteTitle } from '@@/utils/paths'
|
||||||
import { nextTick, ref } from 'vue'
|
import { nextTick, ref } from 'vue'
|
||||||
import { closeTab, createNote, toggleTabPin, ws } from '../workspace'
|
import { closeTab, createNote, ws } from '../workspace'
|
||||||
|
|
||||||
// Drag sorting: dropping on a tab's left half inserts before it; its right half inserts after it.
|
// Drag sorting: dropping on a tab's left half inserts before it; its right half inserts after it.
|
||||||
const TAB_TYPE = 'application/x-rawmark-tab'
|
const TAB_TYPE = 'application/x-rawmark-tab'
|
||||||
@@ -113,7 +113,7 @@ function closeRight(path: string) {
|
|||||||
<template v-if="menu">
|
<template v-if="menu">
|
||||||
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
||||||
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: `${menu.x}px`, top: `${menu.y}px` }">
|
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: `${menu.x}px`, top: `${menu.y}px` }">
|
||||||
<button @click="toggleTabPin(menu.tab.path); menu = null">
|
<button @click="menu.tab.pinned = !menu.tab.pinned; menu = null">
|
||||||
{{ menu.tab.pinned ? translate('workspace.unpin_20590a') : translate('workspace.pin_d4b3f4') }}
|
{{ menu.tab.pinned ? translate('workspace.unpin_20590a') : translate('workspace.pin_d4b3f4') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="closeTab(menu.tab.path); menu = null">
|
<button @click="closeTab(menu.tab.path); menu = null">
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { ApiError } from '@/http/axios'
|
import { ApiError } from '@/http/axios'
|
||||||
|
|
||||||
import { pinia } from '@/pinia'
|
import { pinia } from '@/pinia'
|
||||||
import { useSettingsStore } from '@/pinia/stores/settings'
|
|
||||||
import { useUserStore } from '@/pinia/stores/user'
|
import { useUserStore } from '@/pinia/stores/user'
|
||||||
import { apiSuccess } from '../../../tests/api-response'
|
import { apiSuccess } from '../../../tests/api-response'
|
||||||
import { pluginEvents } from '../../common/plugins/registry'
|
import { pluginEvents } from '../../common/plugins/registry'
|
||||||
import { drafts } from './drafts'
|
import { drafts } from './drafts'
|
||||||
import { insertImageFiles } from './editor/paste'
|
import { insertImageFiles } from './editor/paste'
|
||||||
import { attachView, closeTab, createNoteAt, editorStateOf, flush, importNotes, keepLocalVersion, move, openNote, readyToReload, refreshTree, remove, replaceImage, restore, startWorkspace, stopWorkspace, storeViewState, toggleTabPin, ws } from './workspace'
|
import { attachView, closeTab, createNoteAt, editorStateOf, flush, importNotes, keepLocalVersion, move, openNote, readyToReload, refreshTree, remove, replaceImage, restore, startWorkspace, stopWorkspace, storeViewState, ws } from './workspace'
|
||||||
|
|
||||||
vi.mock('@@/apis/events', () => ({ connectEventsApi: vi.fn(() => () => {}) }))
|
vi.mock('@@/apis/events', () => ({ connectEventsApi: vi.fn(() => () => {}) }))
|
||||||
|
|
||||||
@@ -117,249 +116,6 @@ describe('事件同步', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('固定标签跨设备同步', () => {
|
|
||||||
const { settings, flushSettings } = useSettingsStore(pinia)
|
|
||||||
let serverPins: string[] | undefined
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
serverPins = undefined
|
|
||||||
settings.pinnedTabs = undefined
|
|
||||||
localStorage.removeItem('rawmark.tabs')
|
|
||||||
vi.spyOn(treeApi, 'getTreeApi').mockResolvedValue(apiSuccess({ entries: ['a.md', 'b.md', 'from/note.md'].map(path => ({ path, dir: false, size: 0, mtime: 0 })) }))
|
|
||||||
vi.spyOn(settingsApi, 'getSettingsApi').mockImplementation(async () => apiSuccess(serverPins === undefined ? {} : { pinnedTabs: [...serverPins] }))
|
|
||||||
vi.spyOn(settingsApi, 'patchSettingsApi').mockImplementation(async (patch) => {
|
|
||||||
serverPins = patch.pinnedTabs as string[]
|
|
||||||
return apiSuccess({ pinnedTabs: serverPins })
|
|
||||||
})
|
|
||||||
vi.spyOn(fontsApi, 'getFontsApi').mockResolvedValue(apiSuccess({ fonts: [] }))
|
|
||||||
vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [] }))
|
|
||||||
vi.spyOn(noteFiles, 'readNoteFile').mockResolvedValue({ content: '服务器正文', rev: 'r1' })
|
|
||||||
vi.spyOn(drafts, 'all').mockResolvedValue([])
|
|
||||||
vi.spyOn(drafts, 'delete').mockResolvedValue(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
stopWorkspace()
|
|
||||||
await flushSettings().catch(() => {})
|
|
||||||
await flushPromises()
|
|
||||||
settings.pinnedTabs = undefined
|
|
||||||
localStorage.removeItem('rawmark.tabs')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('固定后保存服务器,另一设备没有本地标签也能恢复', async () => {
|
|
||||||
await startWorkspace()
|
|
||||||
await openNote('a.md')
|
|
||||||
await toggleTabPin('a.md')
|
|
||||||
expect(serverPins).toEqual(['a.md'])
|
|
||||||
expect(ws.tabs[0].pinned).toBe(true)
|
|
||||||
stopWorkspace()
|
|
||||||
await flushPromises()
|
|
||||||
localStorage.removeItem('rawmark.tabs')
|
|
||||||
settings.pinnedTabs = undefined
|
|
||||||
await startWorkspace()
|
|
||||||
expect(ws.tabs.map(tab => [tab.path, tab.pinned])).toEqual([['a.md', true]])
|
|
||||||
expect(editorStateOf('a.md')?.doc.toString()).toBe('服务器正文')
|
|
||||||
expect(ws.active).toBe('a.md')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('只在服务器尚无固定列表时迁移本地固定标签', async () => {
|
|
||||||
localStorage.setItem('rawmark.tabs', JSON.stringify({ paths: ['a.md'], active: 'a.md', pinned: ['a.md'] }))
|
|
||||||
await startWorkspace()
|
|
||||||
expect(serverPins).toEqual(['a.md'])
|
|
||||||
expect(settingsApi.patchSettingsApi).toHaveBeenCalledTimes(1)
|
|
||||||
await flushPromises()
|
|
||||||
expect(JSON.parse(localStorage.getItem('rawmark.tabs')!)).not.toHaveProperty('pinned')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('服务器已取消所有固定时,旧设备缓存不能重新固定', async () => {
|
|
||||||
serverPins = []
|
|
||||||
localStorage.setItem('rawmark.tabs', JSON.stringify({ paths: ['a.md'], active: 'a.md', pinned: ['a.md'] }))
|
|
||||||
await startWorkspace()
|
|
||||||
expect(ws.tabs[0].pinned).toBe(false)
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('加载服务器失败时保留本地固定状态,不上传未确认的列表', async () => {
|
|
||||||
vi.mocked(settingsApi.getSettingsApi).mockRejectedValueOnce(new ApiError(503, '设置暂不可用'))
|
|
||||||
localStorage.setItem('rawmark.tabs', JSON.stringify({ paths: ['a.md'], active: 'a.md', pinned: ['a.md'] }))
|
|
||||||
await startWorkspace()
|
|
||||||
expect(ws.tabs[0].pinned).toBe(true)
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
expect(JSON.parse(localStorage.getItem('rawmark.tabs')!).pinned).toEqual(['a.md'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('其他设备固定和取消固定,不切换视图或关闭本机未保存标签', async () => {
|
|
||||||
serverPins = ['a.md']
|
|
||||||
await startWorkspace()
|
|
||||||
const tab = ws.tabs[0]
|
|
||||||
tab.dirty = true
|
|
||||||
const state = editorStateOf('a.md')
|
|
||||||
ws.view = 'graph'
|
|
||||||
ws.images = ''
|
|
||||||
ws.sidebarOpen = true
|
|
||||||
serverPins = ['b.md']
|
|
||||||
vi.mocked(connectEventsApi).mock.lastCall![0].onEvents([{ type: 'settings' }])
|
|
||||||
await vi.waitFor(() => expect(ws.tabs.find(t => t.path === 'b.md')?.status).toBe('ready'))
|
|
||||||
expect(ws.active).toBe('a.md')
|
|
||||||
expect(ws.view).toBe('graph')
|
|
||||||
expect(ws.images).toBe('')
|
|
||||||
expect(ws.sidebarOpen).toBe(true)
|
|
||||||
expect(tab.pinned).toBe(false)
|
|
||||||
expect(tab.dirty).toBe(true)
|
|
||||||
expect(editorStateOf('a.md')).toBe(state)
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
tab.dirty = false
|
|
||||||
})
|
|
||||||
|
|
||||||
it('断线重连补回错过的固定设置', async () => {
|
|
||||||
serverPins = []
|
|
||||||
await startWorkspace()
|
|
||||||
serverPins = ['b.md']
|
|
||||||
vi.mocked(connectEventsApi).mock.lastCall![0].onResync()
|
|
||||||
await vi.waitFor(() => expect(ws.tabs.find(tab => tab.path === 'b.md')?.pinned).toBe(true))
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('保存失败时提示并恢复服务器状态', async () => {
|
|
||||||
serverPins = []
|
|
||||||
await startWorkspace()
|
|
||||||
await openNote('a.md')
|
|
||||||
vi.mocked(settingsApi.patchSettingsApi).mockRejectedValueOnce(new ApiError(503, '固定保存失败'))
|
|
||||||
await toggleTabPin('a.md')
|
|
||||||
expect(ws.tabs[0].pinned).toBe(false)
|
|
||||||
expect(settings.pinnedTabs).toEqual([])
|
|
||||||
expect(toasts.some(toast => toast.text.includes('固定保存失败'))).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('快速固定和取消固定按操作顺序保存', async () => {
|
|
||||||
serverPins = []
|
|
||||||
await startWorkspace()
|
|
||||||
await openNote('a.md')
|
|
||||||
await Promise.all([toggleTabPin('a.md'), toggleTabPin('a.md'), toggleTabPin('a.md')])
|
|
||||||
expect(vi.mocked(settingsApi.patchSettingsApi).mock.calls.map(([patch]) => patch.pinnedTabs)).toEqual([['a.md'], [], ['a.md']])
|
|
||||||
expect(serverPins).toEqual(['a.md'])
|
|
||||||
expect(ws.tabs[0].pinned).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('主动关闭固定标签也从服务器固定列表中移除', async () => {
|
|
||||||
serverPins = ['a.md', 'b.md']
|
|
||||||
await startWorkspace()
|
|
||||||
expect(await closeTab('a.md')).toBe(true)
|
|
||||||
expect(serverPins).toEqual(['b.md'])
|
|
||||||
expect(ws.tabs.some(tab => tab.path === 'a.md')).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('取消固定保存失败时保留标签,不丢失固定状态', async () => {
|
|
||||||
serverPins = ['a.md']
|
|
||||||
await startWorkspace()
|
|
||||||
vi.mocked(settingsApi.patchSettingsApi).mockRejectedValueOnce(new ApiError(503, '保存失败'))
|
|
||||||
expect(await closeTab('a.md')).toBe(false)
|
|
||||||
expect(ws.tabs.map(tab => [tab.path, tab.pinned])).toEqual([['a.md', true]])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('关闭时草稿保存失败,保留标签和服务器固定记录', async () => {
|
|
||||||
serverPins = ['a.md']
|
|
||||||
await startWorkspace()
|
|
||||||
const tab = ws.tabs[0]
|
|
||||||
Object.assign(tab, { status: 'conflict', dirty: true })
|
|
||||||
const put = vi.spyOn(drafts, 'put').mockRejectedValueOnce(new Error('草稿存储不可用'))
|
|
||||||
expect(await closeTab('a.md')).toBe(false)
|
|
||||||
expect(ws.tabs).toContain(tab)
|
|
||||||
expect(tab.pinned).toBe(true)
|
|
||||||
expect(serverPins).toEqual(['a.md'])
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
put.mockResolvedValue('a.md')
|
|
||||||
tab.dirty = false
|
|
||||||
})
|
|
||||||
|
|
||||||
it('取消固定请求期间又输入,保留新内容并恢复固定', async () => {
|
|
||||||
serverPins = ['a.md']
|
|
||||||
await startWorkspace()
|
|
||||||
const v = await edit('a.md')
|
|
||||||
const original = v.state.doc.toString()
|
|
||||||
vi.spyOn(drafts, 'put').mockResolvedValue('a.md')
|
|
||||||
let finish!: () => void
|
|
||||||
vi.mocked(settingsApi.patchSettingsApi).mockImplementationOnce(async (patch) => {
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
finish = resolve
|
|
||||||
})
|
|
||||||
serverPins = patch.pinnedTabs as string[]
|
|
||||||
return apiSuccess({ pinnedTabs: serverPins })
|
|
||||||
})
|
|
||||||
const closing = closeTab('a.md')
|
|
||||||
await vi.waitFor(() => expect(finish).toBeTypeOf('function'))
|
|
||||||
v.dispatch({ changes: { from: 0, insert: '新增输入' } })
|
|
||||||
finish()
|
|
||||||
expect(await closing).toBe(false)
|
|
||||||
expect(serverPins).toEqual(['a.md'])
|
|
||||||
expect(ws.tabs[0].pinned).toBe(true)
|
|
||||||
expect(ws.tabs[0].dirty).toBe(true)
|
|
||||||
expect(v.state.doc.toString()).toBe(`新增输入${original}`)
|
|
||||||
ws.tabs[0].dirty = false
|
|
||||||
})
|
|
||||||
|
|
||||||
it('重置设置后同步取消固定,重新登录不会迁回旧记录', async () => {
|
|
||||||
serverPins = ['a.md']
|
|
||||||
await startWorkspace()
|
|
||||||
const tab = ws.tabs[0]
|
|
||||||
tab.dirty = true
|
|
||||||
const state = editorStateOf('a.md')
|
|
||||||
serverPins = []
|
|
||||||
vi.mocked(connectEventsApi).mock.lastCall![0].onEvents([{ type: 'settings' }])
|
|
||||||
await vi.waitFor(() => expect(tab.pinned).toBe(false))
|
|
||||||
expect(ws.tabs).toContain(tab)
|
|
||||||
expect(tab.dirty).toBe(true)
|
|
||||||
expect(editorStateOf('a.md')).toBe(state)
|
|
||||||
await flushPromises()
|
|
||||||
expect(JSON.parse(localStorage.getItem('rawmark.tabs')!)).not.toHaveProperty('pinned')
|
|
||||||
tab.dirty = false
|
|
||||||
stopWorkspace()
|
|
||||||
await flushPromises()
|
|
||||||
// Another device may still hold a legacy cache; the reset must remain authoritative.
|
|
||||||
localStorage.setItem('rawmark.tabs', JSON.stringify({ paths: ['a.md'], active: 'a.md', pinned: ['a.md'] }))
|
|
||||||
await startWorkspace()
|
|
||||||
expect(ws.tabs[0].pinned).toBe(false)
|
|
||||||
expect(settingsApi.patchSettingsApi).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('在本机重命名固定笔记后,新设备仍能按新路径恢复', async () => {
|
|
||||||
serverPins = ['from/note.md']
|
|
||||||
await startWorkspace()
|
|
||||||
vi.spyOn(filesApi, 'moveFileApi').mockResolvedValue(apiSuccess({ ok: true }))
|
|
||||||
vi.mocked(treeApi.getTreeApi).mockResolvedValue(apiSuccess({ entries: [{ path: 'to/note.md', dir: false, size: 0, mtime: 0 }] }))
|
|
||||||
expect(await move('from', 'to')).toEqual({ ok: true })
|
|
||||||
expect(serverPins).toEqual(['to/note.md'])
|
|
||||||
stopWorkspace()
|
|
||||||
await flushPromises()
|
|
||||||
localStorage.removeItem('rawmark.tabs')
|
|
||||||
await startWorkspace()
|
|
||||||
expect(ws.tabs.map(tab => [tab.path, tab.pinned])).toEqual([['to/note.md', true]])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('目录重命名后,服务器固定列表跟随新路径', async () => {
|
|
||||||
serverPins = ['from/note.md']
|
|
||||||
await startWorkspace()
|
|
||||||
vi.mocked(treeApi.getTreeApi).mockResolvedValue(apiSuccess({ entries: [{ path: 'to/note.md', dir: false, size: 0, mtime: 0 }] }))
|
|
||||||
vi.mocked(connectEventsApi).mock.lastCall![0].onEvents([{ type: 'moved', from: 'from', path: 'to' }, { type: 'tree' }])
|
|
||||||
await vi.waitFor(() => expect(serverPins).toEqual(['to/note.md']))
|
|
||||||
await vi.waitFor(() => expect(ws.tabs.map(tab => [tab.path, tab.pinned])).toEqual([['to/note.md', true]]))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('旧工作区未完成的标签读取不覆盖新工作区同名笔记', async () => {
|
|
||||||
let finish!: (result: { content: string, rev: string }) => void
|
|
||||||
vi.mocked(noteFiles.readNoteFile).mockImplementationOnce(() => new Promise((resolve) => {
|
|
||||||
finish = resolve
|
|
||||||
}))
|
|
||||||
const opening = openNote('a.md', { background: true })
|
|
||||||
stopWorkspace()
|
|
||||||
await openNote('a.md')
|
|
||||||
finish({ content: '旧工作区正文', rev: 'old' })
|
|
||||||
await opening
|
|
||||||
expect(editorStateOf('a.md')?.doc.toString()).toBe('服务器正文')
|
|
||||||
expect(ws.tabs[0].rev).toBe('r1')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function edit(path: string) {
|
async function edit(path: string) {
|
||||||
vi.spyOn(noteFiles, 'readNoteFile').mockResolvedValue({ content: '原文', rev: 'r1' })
|
vi.spyOn(noteFiles, 'readNoteFile').mockResolvedValue({ content: '原文', rev: 'r1' })
|
||||||
vi.spyOn(drafts, 'delete').mockResolvedValue(undefined)
|
vi.spyOn(drafts, 'delete').mockResolvedValue(undefined)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CanceledError, isCancel } from 'axios'
|
|||||||
import { nextTick, reactive, watch } from 'vue'
|
import { nextTick, reactive, watch } from 'vue'
|
||||||
import { ApiError } from '@/http/axios'
|
import { ApiError } from '@/http/axios'
|
||||||
import { pinia } from '@/pinia'
|
import { pinia } from '@/pinia'
|
||||||
import { SettingsAppearanceError, useSettingsStore } from '@/pinia/stores/settings'
|
import { useSettingsStore } from '@/pinia/stores/settings'
|
||||||
import { useUserStore } from '@/pinia/stores/user'
|
import { useUserStore } from '@/pinia/stores/user'
|
||||||
import { drafts } from './drafts'
|
import { drafts } from './drafts'
|
||||||
import { createEditorState, insertImageFiles, languageCompartment, pasteImages, pluginCompartment, pluginEditorExtensions, pluginLanguage, preferencesCompartment, previewCompartment, previewExtensions } from './editor'
|
import { createEditorState, insertImageFiles, languageCompartment, pasteImages, pluginCompartment, pluginEditorExtensions, pluginLanguage, preferencesCompartment, previewCompartment, previewExtensions } from './editor'
|
||||||
@@ -38,7 +38,7 @@ import { editorLocale, localeCompartment } from './editor/locale'
|
|||||||
import { editorCommands } from './editor/shortcuts'
|
import { editorCommands } from './editor/shortcuts'
|
||||||
import { refreshMetadata } from './metadata'
|
import { refreshMetadata } from './metadata'
|
||||||
|
|
||||||
const { loadSettings, settings, updateSettings } = useSettingsStore(pinia)
|
const { loadSettings, settings } = useSettingsStore(pinia)
|
||||||
|
|
||||||
export type TabStatus = 'loading' | 'ready' | 'conflict' | 'deleted' | 'error'
|
export type TabStatus = 'loading' | 'ready' | 'conflict' | 'deleted' | 'error'
|
||||||
|
|
||||||
@@ -515,90 +515,35 @@ export function revealPending(v: EditorView, path: string): boolean {
|
|||||||
|
|
||||||
// ---- Tabs ----
|
// ---- Tabs ----
|
||||||
|
|
||||||
export async function openNote(path: string, options?: { background?: boolean }) {
|
export async function openNote(path: string) {
|
||||||
const version = user.getSessionVersion()
|
const version = user.getSessionVersion()
|
||||||
const epoch = workspaceEpoch
|
|
||||||
if (!options?.background) {
|
|
||||||
ws.view = 'notes'
|
ws.view = 'notes'
|
||||||
ws.images = null
|
ws.images = null
|
||||||
ws.sidebarOpen = false
|
ws.sidebarOpen = false
|
||||||
}
|
|
||||||
// New tree notes appear optimistically; wait for actual creation before reading them.
|
// New tree notes appear optimistically; wait for actual creation before reading them.
|
||||||
const pending = creating.get(path)
|
const pending = creating.get(path)
|
||||||
if (pending && !(await pending)) return
|
if (pending && !(await pending)) return
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
if (tabOf(path)) {
|
if (tabOf(path)) {
|
||||||
if (!options?.background) ws.active = path
|
ws.active = path
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const tab = reactive<Tab>({ path, pinned: settings.pinnedTabs?.includes(path) ?? false, rev: '', status: 'loading', dirty: false })
|
const tab: Tab = { path, rev: '', status: 'loading', dirty: false }
|
||||||
ws.tabs.push(tab)
|
ws.tabs.push(tab)
|
||||||
if (!options?.background) ws.active = path
|
ws.active = path
|
||||||
try {
|
try {
|
||||||
const { content, rev } = await readNoteFile(path)
|
const { content, rev } = await readNoteFile(path)
|
||||||
user.assertSession(version)
|
|
||||||
if (epoch !== workspaceEpoch || tabOf(path) !== tab) return
|
|
||||||
editorStates.set(path, buildState(path, content))
|
editorStates.set(path, buildState(path, content))
|
||||||
const t = tabOf(path)
|
const t = tabOf(path)
|
||||||
if (t) Object.assign(t, { rev, status: 'ready' })
|
if (t) Object.assign(t, { rev, status: 'ready' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isCancel(err)) return
|
if (isCancel(err)) return
|
||||||
if (epoch !== workspaceEpoch || tabOf(path) !== tab) return
|
|
||||||
const t = tabOf(path)
|
const t = tabOf(path)
|
||||||
if (t) t.status = 'error'
|
if (t) t.status = 'error'
|
||||||
toast(translate('workspace.could_not_open_0ed1e1', { p0: errorText(err) }))
|
toast(translate('workspace.could_not_open_0ed1e1', { p0: errorText(err) }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncPinnedTabs() {
|
|
||||||
if (settings.pinnedTabs === undefined) return
|
|
||||||
const pinned = new Set(settings.pinnedTabs)
|
|
||||||
for (const tab of ws.tabs) tab.pinned = pinned.has(tab.path)
|
|
||||||
// Remote unpins never close tabs or discard local drafts, and remote pins never steal focus.
|
|
||||||
const notes = new Set(ws.entries.filter(entry => !entry.dir && entry.path.endsWith(NOTE_EXT)).map(entry => entry.path))
|
|
||||||
await Promise.all([...pinned].filter(path => notes.has(path) && !tabOf(path)).map(path => openNote(path, { background: true })))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncSettings() {
|
|
||||||
await loadSettings()
|
|
||||||
await syncPinnedTabs()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePinnedTabs(paths: string[]): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const saving = updateSettings({ pinnedTabs: paths })
|
|
||||||
for (const tab of ws.tabs) tab.pinned = paths.includes(tab.path)
|
|
||||||
await saving
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
|
||||||
if (isCancel(err)) return false
|
|
||||||
if (err instanceof SettingsAppearanceError) {
|
|
||||||
toast(err.message)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
toast(translate('common.could_not_save_settings_1cdd7f', { p0: err instanceof Error ? err.message : errorText(err) }))
|
|
||||||
try {
|
|
||||||
await syncSettings()
|
|
||||||
} catch (reload) {
|
|
||||||
if (!isCancel(reload)) toast(translate('workspace.could_not_synchronize_settings_7e5bb1', { p0: errorText(reload) }))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function toggleTabPin(path: string) {
|
|
||||||
if (!tabOf(path)) return
|
|
||||||
const pinned = settings.pinnedTabs ?? ws.tabs.filter(tab => tab.pinned).map(tab => tab.path)
|
|
||||||
await savePinnedTabs(pinned.includes(path) ? pinned.filter(p => p !== path) : [...pinned, path])
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remapPinnedTabs(from: string, to: string) {
|
|
||||||
const pinned = settings.pinnedTabs
|
|
||||||
if (pinned?.some(path => remapPath(path, from, to) !== null)) {
|
|
||||||
await savePinnedTabs(pinned.map(path => remapPath(path, from, to) ?? path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function closeTab(path: string): Promise<boolean> {
|
export async function closeTab(path: string): Promise<boolean> {
|
||||||
const version = user.getSessionVersion()
|
const version = user.getSessionVersion()
|
||||||
const busy = () => Boolean(slots.get(path)?.uploads || slots.get(path)?.paused)
|
const busy = () => Boolean(slots.get(path)?.uploads || slots.get(path)?.paused)
|
||||||
@@ -630,19 +575,6 @@ export async function closeTab(path: string): Promise<boolean> {
|
|||||||
if (tabOf(path) !== tab || slot(path).seq !== seq || busy()) return false
|
if (tabOf(path) !== tab || slot(path).seq !== seq || busy()) return false
|
||||||
toast(translate('workspace.unsaved_changes_retained_as_a_8cb2ab'))
|
toast(translate('workspace.unsaved_changes_retained_as_a_8cb2ab'))
|
||||||
}
|
}
|
||||||
if (tab.pinned) {
|
|
||||||
const seq = slot(path).seq
|
|
||||||
const pinned = settings.pinnedTabs ?? ws.tabs.filter(tab => tab.pinned).map(tab => tab.path)
|
|
||||||
if (!(await savePinnedTabs(pinned.filter(p => p !== path)))) return false
|
|
||||||
user.assertSession(version)
|
|
||||||
if (!ws.tabs.includes(tab)) return true
|
|
||||||
// Saving the pin is asynchronous; retain a tab changed by new input or another operation while it was pending.
|
|
||||||
if (tab.path !== path || slot(path).seq !== seq || busy()) {
|
|
||||||
const current = settings.pinnedTabs ?? ws.tabs.filter(tab => tab.pinned).map(tab => tab.path)
|
|
||||||
if (!current.includes(tab.path)) await savePinnedTabs([...current, tab.path])
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const index = ws.tabs.indexOf(tab)
|
const index = ws.tabs.indexOf(tab)
|
||||||
if (index < 0) return true
|
if (index < 0) return true
|
||||||
ws.tabs.splice(index, 1)
|
ws.tabs.splice(index, 1)
|
||||||
@@ -943,8 +875,6 @@ export async function move(from: string, to: string, errorOwner: 'operation' | '
|
|||||||
} finally {
|
} finally {
|
||||||
paused.forEach(s => s.paused--)
|
paused.forEach(s => s.paused--)
|
||||||
}
|
}
|
||||||
await remapPinnedTabs(from, to)
|
|
||||||
user.assertSession(version)
|
|
||||||
// The server rewrites image links when renaming notes, changing their revisions and requiring resynchronization.
|
// The server rewrites image links when renaming notes, changing their revisions and requiring resynchronization.
|
||||||
await Promise.all(ws.tabs.map(t => t.path).map(async (p) => {
|
await Promise.all(ws.tabs.map(t => t.path).map(async (p) => {
|
||||||
const tab = tabOf(p)
|
const tab = tabOf(p)
|
||||||
@@ -1064,7 +994,7 @@ async function handleEvents(evs: VaultEvent[]) {
|
|||||||
if (ev.type === 'indexed') {
|
if (ev.type === 'indexed') {
|
||||||
void refreshMetadata()
|
void refreshMetadata()
|
||||||
} else if (ev.type === 'settings') {
|
} else if (ev.type === 'settings') {
|
||||||
await syncSettings().catch((err) => {
|
void loadSettings().catch((err) => {
|
||||||
if (!isCancel(err)) toast(translate('workspace.could_not_synchronize_settings_7e5bb1', { p0: errorText(err) }))
|
if (!isCancel(err)) toast(translate('workspace.could_not_synchronize_settings_7e5bb1', { p0: errorText(err) }))
|
||||||
})
|
})
|
||||||
await refreshPlugins()
|
await refreshPlugins()
|
||||||
@@ -1094,7 +1024,6 @@ async function handleEvents(evs: VaultEvent[]) {
|
|||||||
if (epoch !== workspaceEpoch) return
|
if (epoch !== workspaceEpoch) return
|
||||||
if (affected(ev.from!).length && !affected(ev.path!).length) remapOpenNotes(ev.from!, ev.path!)
|
if (affected(ev.from!).length && !affected(ev.path!).length) remapOpenNotes(ev.from!, ev.path!)
|
||||||
remapRecent(ev.from!, ev.path!)
|
remapRecent(ev.from!, ev.path!)
|
||||||
await remapPinnedTabs(ev.from!, ev.path!)
|
|
||||||
} finally {
|
} finally {
|
||||||
paused.forEach(s => s.paused--)
|
paused.forEach(s => s.paused--)
|
||||||
}
|
}
|
||||||
@@ -1120,11 +1049,6 @@ async function resync() {
|
|||||||
await refreshTree()
|
await refreshTree()
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
if (epoch !== workspaceEpoch) return
|
if (epoch !== workspaceEpoch) return
|
||||||
await syncSettings().catch((err) => {
|
|
||||||
if (!isCancel(err)) toast(translate('workspace.could_not_synchronize_settings_7e5bb1', { p0: errorText(err) }))
|
|
||||||
})
|
|
||||||
user.assertSession(version)
|
|
||||||
if (epoch !== workspaceEpoch) return
|
|
||||||
await Promise.all(ws.tabs.map(syncOpenTab))
|
await Promise.all(ws.tabs.map(syncOpenTab))
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
if (epoch !== workspaceEpoch) return
|
if (epoch !== workspaceEpoch) return
|
||||||
@@ -1136,10 +1060,6 @@ async function resync() {
|
|||||||
|
|
||||||
const TABS_KEY = 'rawmark.tabs'
|
const TABS_KEY = 'rawmark.tabs'
|
||||||
|
|
||||||
function localTabs() {
|
|
||||||
return { paths: ws.tabs.map(t => t.path), active: ws.active, ...(settings.pinnedTabs === undefined ? { pinned: ws.tabs.filter(t => t.pinned).map(t => t.path) } : {}) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore unsaved drafts: continue saving if the server revision is unchanged; otherwise enter conflict state for user resolution.
|
// Restore unsaved drafts: continue saving if the server revision is unchanged; otherwise enter conflict state for user resolution.
|
||||||
async function recoverDrafts() {
|
async function recoverDrafts() {
|
||||||
const version = user.getSessionVersion()
|
const version = user.getSessionVersion()
|
||||||
@@ -1181,21 +1101,15 @@ export async function startWorkspace() {
|
|||||||
await prepareResources()
|
await prepareResources()
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
// Settings-read failures do not prevent use; retain locally cached values.
|
// Settings-read failures do not prevent use; retain locally cached values.
|
||||||
const settingsLoaded = loadSettings().then(() => true).catch((err) => {
|
const settingsLoaded = loadSettings().catch((err) => {
|
||||||
if (!isCancel(err)) toast(translate('workspace.could_not_load_settings_246415', { p0: errorText(err) }))
|
if (!isCancel(err)) toast(translate('workspace.could_not_load_settings_246415', { p0: errorText(err) }))
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
await refreshTree()
|
await refreshTree()
|
||||||
const confirmedSettings = await settingsLoaded
|
await settingsLoaded
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
const saved = JSON.parse(localStorage.getItem(TABS_KEY) ?? '{"paths":[],"active":null}') as { paths: string[], active: string | null, pinned?: string[] }
|
const saved = JSON.parse(localStorage.getItem(TABS_KEY) ?? '{"paths":[],"active":null}') as { paths: string[], active: string | null, pinned?: string[] }
|
||||||
const exists = new Set(ws.entries.map(e => e.path))
|
const exists = new Set(ws.entries.map(e => e.path))
|
||||||
await Promise.all(saved.paths.filter(p => exists.has(p)).map(path => openNote(path)))
|
await Promise.all(saved.paths.filter(p => exists.has(p)).map(openNote))
|
||||||
if (settings.pinnedTabs === undefined) {
|
|
||||||
for (const tab of ws.tabs) tab.pinned = saved.pinned?.includes(tab.path) ?? false
|
|
||||||
if (confirmedSettings && saved.pinned?.length) await savePinnedTabs(saved.pinned.filter(path => exists.has(path)))
|
|
||||||
}
|
|
||||||
if (settings.pinnedTabs !== undefined) await syncPinnedTabs()
|
|
||||||
try {
|
try {
|
||||||
await recoverDrafts()
|
await recoverDrafts()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1203,9 +1117,9 @@ export async function startWorkspace() {
|
|||||||
toast(translate('workspace.could_not_restore_the_local_a275e8', { p0: errorText(err) }))
|
toast(translate('workspace.could_not_restore_the_local_a275e8', { p0: errorText(err) }))
|
||||||
}
|
}
|
||||||
user.assertSession(version)
|
user.assertSession(version)
|
||||||
|
for (const tab of ws.tabs) tab.pinned = saved.pinned?.includes(tab.path) ?? false
|
||||||
ws.active = saved.active && tabOf(saved.active) ? saved.active : (ws.tabs[0]?.path ?? null)
|
ws.active = saved.active && tabOf(saved.active) ? saved.active : (ws.tabs[0]?.path ?? null)
|
||||||
workspaceRunning = true
|
workspaceRunning = true
|
||||||
localStorage.setItem(TABS_KEY, JSON.stringify(localTabs()))
|
|
||||||
let eventQueue = Promise.resolve()
|
let eventQueue = Promise.resolve()
|
||||||
let stopped = false
|
let stopped = false
|
||||||
const enqueue = (work: () => Promise<void>) => {
|
const enqueue = (work: () => Promise<void>) => {
|
||||||
@@ -1237,10 +1151,12 @@ export async function startWorkspace() {
|
|||||||
if (listenersInstalled) return
|
if (listenersInstalled) return
|
||||||
listenersInstalled = true
|
listenersInstalled = true
|
||||||
watch(
|
watch(
|
||||||
localTabs,
|
() => ({ paths: ws.tabs.map(t => t.path), active: ws.active, pinned: ws.tabs.filter(t => t.pinned).map(t => t.path) }),
|
||||||
(tabs) => {
|
(tabs) => {
|
||||||
if (workspaceRunning) localStorage.setItem(TABS_KEY, JSON.stringify(tabs))
|
if (workspaceRunning) localStorage.setItem(TABS_KEY, JSON.stringify(tabs))
|
||||||
},
|
},
|
||||||
|
// Record tabs opened during startup immediately, including while waiting for tree refresh.
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
() => ws.active,
|
() => ws.active,
|
||||||
|
|||||||
@@ -78,17 +78,6 @@ async function search(selector: string, query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('设置查找与重置', () => {
|
describe('设置查找与重置', () => {
|
||||||
it('服务器固定标签去重并过滤无效数据,区分首次迁移与明确清空', async () => {
|
|
||||||
vi.mocked(settingsApi.getSettingsApi).mockResolvedValueOnce(apiSuccess({ pinnedTabs: ['a.md', 1, '', 'a.md', '目录/b.md'] }))
|
|
||||||
await loadSettings()
|
|
||||||
expect(settings.pinnedTabs).toEqual(['a.md', '目录/b.md'])
|
|
||||||
vi.mocked(settingsApi.getSettingsApi).mockResolvedValueOnce(apiSuccess({ pinnedTabs: [] }))
|
|
||||||
await loadSettings()
|
|
||||||
expect(settings.pinnedTabs).toEqual([])
|
|
||||||
await loadSettings()
|
|
||||||
expect(settings.pinnedTabs).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('分类按约定排序,令牌归入账户与安全', async () => {
|
it('分类按约定排序,令牌归入账户与安全', async () => {
|
||||||
ws.settings = 'account'
|
ws.settings = 'account'
|
||||||
vi.spyOn(tokensApi, 'getTokensApi').mockResolvedValue(apiSuccess({ tokens: [] }))
|
vi.spyOn(tokensApi, 'getTokensApi').mockResolvedValue(apiSuccess({ tokens: [] }))
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const CONTENT_WIDTHS = [
|
|||||||
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
|
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
|
||||||
|
|
||||||
export const DEFAULTS = { locale: DEFAULT_LOCALE, trashRetentionDays: 30, hiddenFonts: [] as string[], selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
|
export const DEFAULTS = { locale: DEFAULT_LOCALE, trashRetentionDays: 30, hiddenFonts: [] as string[], selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
|
||||||
export type Settings = typeof DEFAULTS & Appearance & { shortcuts: Record<string, string[]>, pinnedTabs?: string[] }
|
export type Settings = typeof DEFAULTS & Appearance & { shortcuts: Record<string, string[]> }
|
||||||
|
|
||||||
const CACHE_KEY = 'rawmark.settings'
|
const CACHE_KEY = 'rawmark.settings'
|
||||||
|
|
||||||
@@ -55,8 +55,6 @@ function sanitize(raw: Record<string, unknown>): Settings {
|
|||||||
return {
|
return {
|
||||||
locale: isLocale(raw.locale) ? raw.locale : DEFAULT_LOCALE,
|
locale: isLocale(raw.locale) ? raw.locale : DEFAULT_LOCALE,
|
||||||
hiddenFonts,
|
hiddenFonts,
|
||||||
// Absence permits a one-time migration from browser-only pinned tabs; an empty array is authoritative.
|
|
||||||
pinnedTabs: raw.pinnedTabs === undefined ? undefined : [...new Set(Array.isArray(raw.pinnedTabs) ? raw.pinnedTabs.filter((path): path is string => typeof path === 'string' && path.length > 0) : [])],
|
|
||||||
trashRetentionDays: Number.isSafeInteger(raw.trashRetentionDays) && (raw.trashRetentionDays as number) >= 0 ? raw.trashRetentionDays as number : DEFAULTS.trashRetentionDays,
|
trashRetentionDays: Number.isSafeInteger(raw.trashRetentionDays) && (raw.trashRetentionDays as number) >= 0 ? raw.trashRetentionDays as number : DEFAULTS.trashRetentionDays,
|
||||||
...sanitizeDisplaySettings(raw, customFonts.value.map(font => font.id), hiddenFonts),
|
...sanitizeDisplaySettings(raw, customFonts.value.map(font => font.id), hiddenFonts),
|
||||||
selectionToolbar: typeof raw.selectionToolbar === 'boolean' ? raw.selectionToolbar : true,
|
selectionToolbar: typeof raw.selectionToolbar === 'boolean' ? raw.selectionToolbar : true,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
const CACHE = 'rawmark-shell-__VERSION__'
|
const CACHE = 'rawmark-shell-__VERSION__'
|
||||||
const FILES = __PRECACHE__
|
const FILES = __PRECACHE__
|
||||||
const paths = new Set(FILES)
|
const paths = new Set(FILES)
|
||||||
const isFont = path => /\.(?:woff2?|ttf|otf)$/.test(path)
|
|
||||||
const shell = FILES.filter(path => !isFont(path))
|
|
||||||
|
|
||||||
globalThis.addEventListener('install', (event) => {
|
globalThis.addEventListener('install', (event) => {
|
||||||
event.waitUntil((async () => {
|
event.waitUntil((async () => {
|
||||||
const cache = await caches.open(CACHE)
|
const cache = await caches.open(CACHE)
|
||||||
// Download in batches instead of requesting hundreds of syntax packages concurrently.
|
// Download in batches instead of requesting hundreds of syntax packages and fonts concurrently.
|
||||||
for (let i = 0; i < shell.length; i += 16) {
|
for (let i = 0; i < FILES.length; i += 16) {
|
||||||
await cache.addAll(shell.slice(i, i + 16).map(url => new Request(url, { cache: 'reload' })))
|
await cache.addAll(FILES.slice(i, i + 16).map(url => new Request(url, { cache: 'reload' })))
|
||||||
}
|
}
|
||||||
})())
|
})())
|
||||||
})
|
})
|
||||||
@@ -69,11 +67,6 @@ globalThis.addEventListener('fetch', (event) => {
|
|||||||
if (previous) return previous
|
if (previous) return previous
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const response = await fetch(request)
|
return fetch(request)
|
||||||
// Cache public build fonts only after use; private resource tickets never reach this branch.
|
|
||||||
if (paths.has(url.pathname) && isFont(url.pathname) && response.ok) {
|
|
||||||
event.waitUntil(cache.put(url.pathname, response.clone()))
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
})())
|
})())
|
||||||
})
|
})
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { runInNewContext } from 'node:vm'
|
|
||||||
import { expect, it, vi } from 'vitest'
|
|
||||||
|
|
||||||
function worker() {
|
|
||||||
const handlers: Record<string, (event: { request: Request, waitUntil: (promise: Promise<unknown>) => void, respondWith: (promise: Promise<Response>) => void }) => void> = {}
|
|
||||||
const cache = { addAll: vi.fn().mockResolvedValue(undefined), match: vi.fn<() => Promise<Response | undefined>>().mockResolvedValue(undefined), put: vi.fn().mockResolvedValue(undefined) }
|
|
||||||
const fetch = vi.fn().mockResolvedValue(new Response('font'))
|
|
||||||
const source = readFileSync(new URL('./service-worker.js', import.meta.url), 'utf8').replace('__VERSION__', 'test').replace('__PRECACHE__', JSON.stringify(['/index.html', '/assets/main.js', '/assets/regular.woff2', '/assets/bold.woff2']))
|
|
||||||
runInNewContext(source, { URL, Request: class extends Request {
|
|
||||||
constructor(url: string, options?: RequestInit) {
|
|
||||||
super(new URL(url, 'https://test.invalid'), options)
|
|
||||||
}
|
|
||||||
}, fetch, caches: { open: async () => cache, keys: async () => [] }, globalThis: { location: { origin: 'https://test.invalid' }, addEventListener: (name: string, handler: (event: { request: Request, waitUntil: (promise: Promise<unknown>) => void, respondWith: (promise: Promise<Response>) => void }) => void) => {
|
|
||||||
handlers[name] = handler
|
|
||||||
} } })
|
|
||||||
return { handlers, cache, fetch }
|
|
||||||
}
|
|
||||||
it('安装缓存外壳但不预下载字体', async () => {
|
|
||||||
const { handlers, cache } = worker()
|
|
||||||
let done!: Promise<unknown>
|
|
||||||
handlers.install!({ request: new Request('https://test.invalid'), respondWith: vi.fn(), waitUntil: (promise: Promise<unknown>) => {
|
|
||||||
done = promise
|
|
||||||
} })
|
|
||||||
await done
|
|
||||||
expect(cache.addAll.mock.calls.flat(2).map((request: Request) => request.url)).toEqual(['https://test.invalid/index.html', 'https://test.invalid/assets/main.js'])
|
|
||||||
})
|
|
||||||
it('使用后缓存公开构建字体,缓存命中不再请求网络', async () => {
|
|
||||||
const { handlers, cache, fetch } = worker()
|
|
||||||
const pending: Promise<unknown>[] = []
|
|
||||||
let response!: Promise<Response>
|
|
||||||
const event = { request: new Request('https://test.invalid/assets/bold.woff2'), respondWith: (promise: Promise<Response>) => {
|
|
||||||
response = promise
|
|
||||||
}, waitUntil: (promise: Promise<unknown>) => pending.push(promise) }
|
|
||||||
handlers.fetch!(event); expect((await response).status).toBe(200); await Promise.all(pending)
|
|
||||||
expect(cache.put).toHaveBeenCalledOnce()
|
|
||||||
cache.match.mockResolvedValue(new Response('cached'))
|
|
||||||
handlers.fetch!(event); expect(await (await response).text()).toBe('cached')
|
|
||||||
expect(fetch).toHaveBeenCalledOnce()
|
|
||||||
})
|
|
||||||
it('不缓存失败响应,不拦截私有字体票据', async () => {
|
|
||||||
const { handlers, cache, fetch } = worker()
|
|
||||||
fetch.mockResolvedValue(new Response('missing', { status: 404 }))
|
|
||||||
let response!: Promise<Response>
|
|
||||||
const respondWith = vi.fn((promise: Promise<Response>) => {
|
|
||||||
response = promise
|
|
||||||
})
|
|
||||||
handlers.fetch!({ request: new Request('https://test.invalid/assets/regular.woff2'), respondWith, waitUntil: vi.fn() })
|
|
||||||
expect((await response).status).toBe(404); expect(cache.put).not.toHaveBeenCalled()
|
|
||||||
respondWith.mockClear()
|
|
||||||
handlers.fetch!({ request: new Request('https://test.invalid/api/v1/resources/ticket/fonts/custom/file'), respondWith, waitUntil: vi.fn() })
|
|
||||||
expect(respondWith).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
Reference in new issue
Block a user