mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
Merge remote-tracking branch 'origin/main' into paragraph-panel-indesign
# Conflicts: # ROADMAP.md
This commit is contained in:
+13
-51
@@ -1,10 +1,7 @@
|
||||
# The CLAUDE.md quality gate (`cargo xtask ci`: fmt, clippy -D warnings, tests, assets, layers,
|
||||
# wasm) on every pull request and every push to main. Today only path-filtered workflows run on
|
||||
# pull requests, so most PRs land with no test run at all.
|
||||
#
|
||||
# Ported from wordcraft#800. Linux only; standard GitHub-hosted runner, no secrets.
|
||||
name: CI
|
||||
|
||||
# Quality gates for every push to main and every pull request (`cargo xtask ci`), through the
|
||||
# suite's shared gate workflow in craft-common-actions.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -14,54 +11,19 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A newer push to a PR branch cancels its older run; every push to main runs to the end.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
# Same craft-fonts commit as release.yml, so the font tests see what a release build embeds.
|
||||
CRAFT_FONTS_REF: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
name: cargo xtask ci
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUST_BACKTRACE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out craft-fonts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: ${{ env.CRAFT_FONTS_REF }}
|
||||
path: craft-fonts
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libxcursor-dev \
|
||||
libxrandr-dev libxi-dev libfontconfig1-dev libfreetype-dev mesa-vulkan-drivers
|
||||
|
||||
- name: Install Rust (stable + clippy, rustfmt, wasm32)
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal --component clippy,rustfmt \
|
||||
--target wasm32-unknown-unknown
|
||||
rustup default stable
|
||||
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Quality gate
|
||||
env:
|
||||
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
|
||||
run: cargo xtask ci
|
||||
# Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
|
||||
uses: storytold/craft-common-actions/.github/workflows/gate.yml@main
|
||||
with:
|
||||
# Same craft-fonts commit as release.yml.
|
||||
craft-fonts-ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
timeout-minutes: 90
|
||||
packages: >-
|
||||
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libxcursor-dev
|
||||
libxrandr-dev libxi-dev libfontconfig1-dev libfreetype-dev mesa-vulkan-drivers
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
|
||||
name: FreeBSD
|
||||
|
||||
# The VM build is slow, so it runs nightly on main and on pushes to `release`, not on every pull
|
||||
# request: a drifted dependency still shows up within a day, and always before a release ships.
|
||||
# PRs that change the FreeBSD workflow itself still run it.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [release]
|
||||
pull_request:
|
||||
paths:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- "**/Cargo.toml"
|
||||
- "crates/fonts/**"
|
||||
- .github/workflows/freebsd.yml
|
||||
schedule:
|
||||
- cron: "27 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
# out for now: PdfCraft saw an intermittent access violation in egui UI tests there.)
|
||||
name: Windows ARM64
|
||||
|
||||
# Nightly on main and on pushes to `release` (dependency and runner-image drift shows up within a
|
||||
# day, and always before a release ships), and on PRs that change the Windows packaging itself.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "3 5 * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [release]
|
||||
pull_request:
|
||||
paths:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- "**/Cargo.toml"
|
||||
- "**/build.rs"
|
||||
- .github/workflows/windows-arm64.yml
|
||||
- packaging/windows/**
|
||||
schedule:
|
||||
- cron: "41 4 * * 1" # weekly, so drift in the runner images shows up
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -40,12 +40,12 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
|
||||
| `docs/images/page-4.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
|
||||
| `docs/images/page-cover.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
|
||||
| `docs/images/ui-spread.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
|
||||
| `crates/render/tests/golden/drop-shadow-spread.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/drop-shadow-text.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/drop-shadow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/feather.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/inner-shadow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/outer-glow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/drop-shadow-spread.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/drop-shadow-text.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/drop-shadow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/feather.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/inner-shadow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `crates/render/tests/golden/outer-glow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/it/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects |
|
||||
| `assets/app-icon/LICENSE.txt` | (licence text) | this project | — | Licence for the app icon |
|
||||
| `assets/app-icon/designcraft-1024.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) |
|
||||
| `assets/app-icon/designcraft-macos-512.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) |
|
||||
|
||||
Generated
+21
-21
@@ -942,7 +942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"designcraft-engine",
|
||||
@@ -964,7 +964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-cli"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-color",
|
||||
"designcraft-compose",
|
||||
@@ -980,7 +980,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-color"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-cms",
|
||||
"kurbo",
|
||||
@@ -992,7 +992,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-compose"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-color",
|
||||
"designcraft-doc",
|
||||
@@ -1012,7 +1012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-doc"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"designcraft-color",
|
||||
@@ -1025,7 +1025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-engine"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"caseless",
|
||||
"craft-atomic",
|
||||
@@ -1064,7 +1064,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-epub"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"designcraft-color",
|
||||
@@ -1074,7 +1074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-fonts"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"core-text",
|
||||
"harfrust 0.13.3",
|
||||
@@ -1087,7 +1087,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-format"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-doc",
|
||||
"designcraft-images",
|
||||
@@ -1099,7 +1099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-geom"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"flo_curves",
|
||||
"kurbo",
|
||||
@@ -1112,7 +1112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-idml"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-color",
|
||||
"designcraft-compose",
|
||||
@@ -1127,7 +1127,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-images"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-cms",
|
||||
"designcraft-color",
|
||||
@@ -1142,7 +1142,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-mcp"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-engine",
|
||||
"designcraft-geom",
|
||||
@@ -1155,7 +1155,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-pdf"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-pdf",
|
||||
@@ -1178,7 +1178,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-pptx"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"designcraft-color",
|
||||
@@ -1196,7 +1196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-render"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"designcraft-color",
|
||||
@@ -1215,7 +1215,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-textimport"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-doc",
|
||||
"quick-xml",
|
||||
@@ -1224,7 +1224,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-tools"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-color",
|
||||
"designcraft-compose",
|
||||
@@ -1236,7 +1236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-ui-egui"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"designcraft-color",
|
||||
@@ -1263,7 +1263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "designcraft-web"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"designcraft-engine",
|
||||
"designcraft-format",
|
||||
@@ -5814,7 +5814,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+6
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/designcraft", "apps/designcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -103,11 +103,16 @@ todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
# Line tables keep file and line in panics and backtraces; full debug info
|
||||
# for every test binary and dependency would multiply the target dir size.
|
||||
# The test profile inherits both settings.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
debug = false
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
@@ -301,6 +301,8 @@ measured); Ready estimated.
|
||||
## Languages
|
||||
|
||||
Interface strings translated, of 1,724 measured. Detail: [docs/localization-parity.md](docs/localization-parity.md).
|
||||
A string without a translation shows in English and fails no check; `cargo xtask i18n-report`
|
||||
(also a `cargo xtask ci` step) lists the missing strings per language.
|
||||
|
||||
| Language | Code | Status | Translated |
|
||||
|---|---|---|---|
|
||||
@@ -349,6 +351,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
Newest first.
|
||||
|
||||
- **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
|
||||
- **2026-10-23** · Translations: a missing string shows in English and fails no test; `cargo xtask i18n-report`, run by `cargo xtask ci`, prints the missing strings per language. Tests keep duplicate-key and placeholder checks and the language mechanics (lookup, context captions, plural captions, right-to-left). No readiness percentage change.
|
||||
- **2026-10-22** · Hyphenation Settings, as in InDesign: the Paragraph panel menu's Hyphenation… (`app.hyphenationDialog`) sets the selected paragraphs' Hyphenate, Words with at Least, After First, Before Last, Hyphen Limit, Hyphenation Zone, the Better Spacing ↔ Fewer Hyphens slider (InDesign's 0–10 steps), Hyphenate Capitalized Words, Last Word and Across Column, with Preview, as one undo step; Paragraph Style Options › Hyphenation has the same form. The panel menu lists its items in InDesign's order. Replicated from a hyphenation tutorial: the same text in Minion Pro breaks into the same lines as InDesign's with hyphenation on, off, and at Fewer Hyphens. Hyphenate Across Column is kept and round-trips through IDML, but composition doesn't apply it yet. No readiness percentage change.
|
||||
- **2026-10-22** · The Paragraph panel is laid out as InDesign's: the nine alignment buttons in groups; indent, spacing and drop cap fields by their icons in two columns, with Space Between Paragraphs Using Same Style (Ignore or a space); Shading and Border with the Paragraph Borders and Shading button and colour; Hyphenate with Do Not Align / Align to Baseline Grid. Its panel menu follows InDesign's order: Paragraph and Single-line Composer, Only Align First Line to Grid, Balance Ragged Lines, Optical Margin Alignment, Justification…, Keep Options…, Span Columns…, Hyphenation…, Drop Caps and Nested Styles…, GREP Styles… (these three now open for the selected paragraphs), Paragraph Rules…, Paragraph Borders and Shading…, Bullets and Numbering…, Restart/Continue Numbering, Define Lists… and the paragraph direction. Convert Bullets and Numbering to Text is listed but not available yet. No readiness percentage change.
|
||||
- **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mod cli;
|
||||
@@ -2,12 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DesignCraft</title>
|
||||
<link data-trunk rel="rust" data-bin="designcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #262626; }
|
||||
#designcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#designcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
|
||||
#designcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #b8b8b8; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{EngineError, Result, Session};
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(noundo "file.exportPdf", "Export PDF…", ["File"], None,
|
||||
"{path?, pages?: \"1-3,5\" | [1,3] (1-based positions; default all), flatten?: high|medium|low|ppi (Transparency Flattener: spreads with transparency are rasterised), spreads?: bool, fullScreen?: bool, bookmarksPanel?: bool, pageLayout?: single|continuous|twoUp|twoUpCover|twoUpContinuous, view?: fitPage|fitWidth|actual, advanceSeconds?: number (interactive PDF), bleed?: bool (document bleed), marks?: bool | {crop?, bleed?, pageInfo?, weight?, offset?}, standard?: \"none\"|\"x4\"|\"a2b\", compressImages?: bool, tagged?: bool (structure tree: stories as paragraphs, figures with alt text), media?: bool (interactive: embed placed video and sound), title?, author?} → {path, bytes, pages, warnings} (no path: {base64, …})",
|
||||
"{path?, pages?: \"1-3,5\" | [1,3] (1-based positions; default all), flatten?: high|medium|low|ppi (Transparency Flattener: spreads with transparency are rasterised), spreads?: bool, fullScreen?: bool, bookmarksPanel?: bool, pageLayout?: single|continuous|twoUp|twoUpCover|twoUpContinuous, view?: fitPage|fitWidth|actual, advanceSeconds?: number (interactive PDF), bleed?: bool (document bleed), marks?: bool | {crop?, bleed?, pageInfo?, weight?, offset?}, standard?: \"none\"|\"x4\"|\"a2b\", compressImages?: bool, tagged?: bool (structure tree: stories as paragraphs, figures with alt text), media?: bool (interactive: embed placed video and sound), title?, author?, created?: int (creation date in Unix seconds; default now, for reproducible files)} → {path, bytes, pages, warnings} (no path: {base64, …})",
|
||||
has_doc, export_pdf),
|
||||
cmd!(noundo "file.exportEpub", "Export EPUB (Reflowable)…", ["File"], None,
|
||||
"{path?, title?, author?, language?: \"en\", identifier?: str (dc:identifier; a stable urn:uuid by default), coverImage?: path (a PNG or JPEG as the cover), tocTitle?: str (the table of contents' title; from the language by default), cover?: bool (the first page as the cover image), embedFonts?: bool (package the stylesheet's fonts whose licence allows it, whole files), fixedLayout?: bool (pre-paginated: each page as an image with its text)} → {path, bytes, warnings} (no path: {base64, bytes, warnings}); warnings name images without alt text, exported as decorative, and fonts that could not be embedded",
|
||||
@@ -371,6 +371,10 @@ pub(crate) fn options(p: &Value, page_count: usize) -> Result<PdfOptions> {
|
||||
Some(s) => Standard::parse(s).ok_or_else(|| bad(ID, format!("unknown standard `{s}` (none, x4, a2b)")))?,
|
||||
None => Standard::None,
|
||||
};
|
||||
let created = match p.get("created") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(v.as_i64().ok_or_else(|| bad(ID, "`created`: Unix seconds, an integer"))?),
|
||||
};
|
||||
Ok(PdfOptions {
|
||||
pages,
|
||||
spreads,
|
||||
@@ -382,6 +386,7 @@ pub(crate) fn options(p: &Value, page_count: usize) -> Result<PdfOptions> {
|
||||
author: str_param(p, "author").map(str::to_string),
|
||||
tagged: p.get("tagged").and_then(Value::as_bool).unwrap_or(false),
|
||||
media: p.get("media").and_then(Value::as_bool).unwrap_or(false),
|
||||
created,
|
||||
..PdfOptions::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,8 +135,9 @@ impl OutputRoot {
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// Match fs::write for a new output: 0666 restricted by the caller's umask.
|
||||
builder.permissions(std::fs::Permissions::from_mode(0o666));
|
||||
// Like fs::write for a new output: request 0666 and let the process umask decide the
|
||||
// effective mode (0644 with the usual 022, 0664 with 002).
|
||||
builder.permissions(std::fs::Permissions::from_mode(0o666)); // NOSONAR
|
||||
}
|
||||
let file = builder.tempfile_in(parent).map_err(|e| path_error(path, e))?;
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Print…",
|
||||
["File"],
|
||||
Some("Cmd+P"),
|
||||
"{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (export and preview submission without printing)} → {printer, copies, pages, command, bytes}",
|
||||
"{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (export and preview submission without printing), created?: int (Unix seconds, as file.exportPdf)} → {printer, copies, pages, command, bytes}",
|
||||
has_doc,
|
||||
print
|
||||
),
|
||||
@@ -95,7 +95,8 @@ mod tests {
|
||||
fn print_submits_the_selected_export_and_reports_refusal() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 4})).unwrap();
|
||||
let params = json!({"printer": "Office (east)", "copies": 2, "pages": "2-3", "marks": true, "bleed": true});
|
||||
// A fixed creation date: both exports would otherwise take the clock's second.
|
||||
let params = json!({"printer": "Office (east)", "copies": 2, "pages": "2-3", "marks": true, "bleed": true, "created": 1_700_000_000});
|
||||
let expected = {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let options = super::super::export::options(¶ms, d.page_count()).unwrap();
|
||||
@@ -118,5 +119,6 @@ mod tests {
|
||||
assert!(refusal.to_string().contains("printer offline"));
|
||||
print_with(&mut s, &json!({"dryRun": true}), |_, _| panic!("dry runs must not spool")).unwrap();
|
||||
assert!(print_with(&mut s, &json!({"pages": "9"}), |_, _| panic!("invalid ranges must not spool")).is_err());
|
||||
assert!(print_with(&mut s, &json!({"created": "today"}), |_, _| panic!("an invalid date must not spool")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use designcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Browser Print must reject native spooler operations without trapping the editor.
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use designcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use designcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod book_safety;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod browser_print;
|
||||
mod character_style_from_selection;
|
||||
mod close_retention;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod collection_state_atomicity;
|
||||
mod combined_close_glyph;
|
||||
mod distribute_locks;
|
||||
mod export_page_input;
|
||||
mod insert_pages_bounds;
|
||||
mod param_keys;
|
||||
mod pdf_backend;
|
||||
mod preflight_resolution;
|
||||
mod primary_frame_columns;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod save_state_atomicity;
|
||||
mod stroke_tint_range;
|
||||
mod text_decorations;
|
||||
mod text_insert_memory;
|
||||
mod text_thread_regressions;
|
||||
mod thread_color_safety;
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
mod export_tag_styles;
|
||||
mod grouped_reading_order;
|
||||
@@ -0,0 +1,4 @@
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod system_fonts;
|
||||
@@ -1,7 +1,5 @@
|
||||
//! A new session finds the installed system fonts by family name (#22). One test in its own
|
||||
//! process, so the process-wide database starts fresh, as it does when the app starts.
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use designcraft_fonts::{FontDb, system_font_dirs};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mod rgba_conversion_contract;
|
||||
@@ -3,8 +3,6 @@
|
||||
//! goes inside the write root. Paths that leave a root (`..`, another folder, a sibling with the
|
||||
//! same prefix, a link) are refused, nothing from a refused file reaches the document, and nothing
|
||||
//! is written outside.
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod automation_roots;
|
||||
@@ -1,5 +0,0 @@
|
||||
//! Run public contracts with the production content-cache hasher.
|
||||
#[path = "support/image_content_cache_contract.rs"]
|
||||
mod contracts;
|
||||
#[path = "support/image_fixtures.rs"]
|
||||
mod image_fixtures;
|
||||
+1
-1
@@ -42,7 +42,7 @@ fn mixed_gradient_export_survives_in_subprocess() {
|
||||
const CHILD: &str = "DESIGNCRAFT_TEST_MIXED_GRADIENT_CHILD";
|
||||
if std::env::var_os(CHILD).is_none() {
|
||||
let out = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "mixed_gradient_export_survives_in_subprocess", "--nocapture"])
|
||||
.args(["--exact", "gradient_color_spaces::mixed_gradient_export_survives_in_subprocess", "--nocapture"])
|
||||
.env(CHILD, "1")
|
||||
.output()
|
||||
.unwrap();
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Run public contracts with the production content-cache hasher.
|
||||
#[path = "../support/image_content_cache_contract.rs"]
|
||||
mod contracts;
|
||||
@@ -0,0 +1,6 @@
|
||||
#[path = "../support/image_fixtures.rs"]
|
||||
mod image_fixtures;
|
||||
|
||||
mod gradient_color_spaces;
|
||||
mod image_content_cache;
|
||||
mod page_entry_search_contract;
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! Goldens live in `tests/golden/*.png` and are compared with a tolerance (antialiasing and blur
|
||||
//! rounding may differ by a few levels between SIMD levels). Regenerate them after an intended
|
||||
//! change with `DESIGNCRAFT_BLESS=1 cargo test -p designcraft-render --test effects`.
|
||||
//! change with `DESIGNCRAFT_BLESS=1 cargo test -p designcraft-render --test it effects::`.
|
||||
|
||||
use designcraft_compose::Cache;
|
||||
use designcraft_doc::build::NewDocument;
|
||||
@@ -0,0 +1,12 @@
|
||||
mod blend_over_paper;
|
||||
mod colorize;
|
||||
mod effects;
|
||||
mod glyph_retention;
|
||||
mod gradient_inks;
|
||||
mod gradients;
|
||||
mod pdf_geometry;
|
||||
mod perf;
|
||||
mod placed_pdf;
|
||||
mod stroke_alignment;
|
||||
mod text_frame_bottom;
|
||||
mod text_stroke_behind;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Renderer caching/culling behaviour, and a render-time regression test (`#[ignore]`: wall-clock
|
||||
//! timings are only meaningful on an idle machine — run with
|
||||
//! `cargo test --release -p designcraft-render --test perf -- --ignored`).
|
||||
//! `cargo test --release -p designcraft-render --test it perf:: -- --ignored`).
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
+118
-125
@@ -4612,6 +4612,12 @@ fn column(lang: &str) -> Option<usize> {
|
||||
|
||||
/// `s` in `lang` (English, or the string itself, when there's no translation).
|
||||
pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
let text = lookup(lang, s);
|
||||
// An empty row is a translation nobody has written yet.
|
||||
if text.trim().is_empty() { s } else { text }
|
||||
}
|
||||
|
||||
fn lookup<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
static GERMAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
static JAPANESE: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
static TRANSLATIONS: OnceLock<HashMap<&'static str, [&'static str; 5]>> = OnceLock::new();
|
||||
@@ -4630,18 +4636,18 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
// The German and Japanese tables cover the whole interface; the shared table's German and Japanese
|
||||
// columns are the fallback for keys added there after them.
|
||||
if lang == "de"
|
||||
&& let Some(text) = GERMAN.get_or_init(|| de::TABLE.iter().copied().collect()).get(s).copied()
|
||||
&& let Some(text) = GERMAN.get_or_init(|| de::TABLE.iter().copied().collect()).get(s).copied().filter(|text| !text.trim().is_empty())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
if lang == "ja"
|
||||
&& let Some(text) = JAPANESE.get_or_init(|| ja::TABLE.iter().copied().collect()).get(s).copied()
|
||||
&& let Some(text) = JAPANESE.get_or_init(|| ja::TABLE.iter().copied().collect()).get(s).copied().filter(|text| !text.trim().is_empty())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
// `fr::TABLE` holds the strings the shared table has no row for.
|
||||
if lang == "fr"
|
||||
&& let Some(text) = FRENCH.get_or_init(|| fr::TABLE.iter().copied().collect()).get(s).copied()
|
||||
&& let Some(text) = FRENCH.get_or_init(|| fr::TABLE.iter().copied().collect()).get(s).copied().filter(|text| !text.trim().is_empty())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
@@ -4687,11 +4693,13 @@ pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str {
|
||||
"ar" => ar::CONTEXT,
|
||||
_ => &[],
|
||||
};
|
||||
if let Some((_, _, text)) = contexts.iter().find(|(key, scope, _)| *key == s && *scope == context) {
|
||||
let written = |text: &&str| !text.trim().is_empty();
|
||||
if let Some(text) = contexts.iter().find(|(key, scope, _)| *key == s && *scope == context).map(|(_, _, text)| *text).filter(written) {
|
||||
return text;
|
||||
}
|
||||
if let Some(c) = column(lang)
|
||||
&& let Some(text) = CONTEXT.iter().find(|(key, scope, _)| *key == s && *scope == context).and_then(|(_, _, row)| row.get(c))
|
||||
&& let Some(text) =
|
||||
CONTEXT.iter().find(|(key, scope, _)| *key == s && *scope == context).and_then(|(_, _, row)| row.get(c)).copied().filter(written)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
@@ -4730,7 +4738,9 @@ pub fn count_label(lang: &str, noun: &str, count: usize) -> String {
|
||||
2..=4 => few,
|
||||
_ => other,
|
||||
};
|
||||
return format!("{count} {form}");
|
||||
if !form.trim().is_empty() {
|
||||
return format!("{count} {form}");
|
||||
}
|
||||
}
|
||||
format!("{count} {}", tr(lang, noun))
|
||||
}
|
||||
@@ -4788,15 +4798,9 @@ mod tests {
|
||||
/// The German and Japanese tables translate the whole interface and win over the shared columns.
|
||||
const WHOLE_INTERFACE: [&str; 2] = ["de", "ja"];
|
||||
|
||||
#[test]
|
||||
fn every_interface_language_has_a_catalog() {
|
||||
for (lang, i) in SHARED_COLUMNS.iter().zip(0..) {
|
||||
assert_eq!(column(lang), Some(i), "{lang}");
|
||||
}
|
||||
for (code, name) in LANGUAGES.iter().filter(|(code, _)| !code.is_empty()) {
|
||||
assert!(!name.is_empty(), "{code}");
|
||||
assert!(LANGUAGE_TABLES.iter().any(|(lang, _)| lang == code) || column(code).is_some(), "{code} has no catalog in LANGUAGE_TABLES");
|
||||
}
|
||||
/// What `tr` shows for a row: an empty one is still untranslated.
|
||||
fn or_english<'a>(text: &'a str, english: &'a str) -> &'a str {
|
||||
if text.trim().is_empty() { english } else { text }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4807,13 +4811,16 @@ mod tests {
|
||||
for (key, translations) in TABLE {
|
||||
for (lang, shared) in SHARED_COLUMNS.into_iter().zip(translations) {
|
||||
// A language's own table is looked up before the shared column.
|
||||
assert_eq!(tr(lang, key), own(lang, key).unwrap_or(*shared), "{lang}: {key}");
|
||||
let expected = own(lang, key).filter(|text| !text.trim().is_empty()).unwrap_or(*shared);
|
||||
assert_eq!(tr(lang, key), or_english(expected, key), "{lang}: {key}");
|
||||
}
|
||||
assert_eq!(tr("", key), *key);
|
||||
}
|
||||
for (lang, table) in LANGUAGE_TABLES {
|
||||
for (key, expected) in *table {
|
||||
assert_eq!(tr(lang, key), *expected, "{lang}: {key}");
|
||||
if !expected.trim().is_empty() {
|
||||
assert_eq!(tr(lang, key), *expected, "{lang}: {key}");
|
||||
}
|
||||
assert_eq!(tr("", key), *key);
|
||||
}
|
||||
}
|
||||
@@ -4830,7 +4837,9 @@ mod tests {
|
||||
fn context_captions_come_from_their_context_or_the_plain_translation() {
|
||||
for (lang, table) in CONTEXT_TABLES {
|
||||
for (key, scope, text) in *table {
|
||||
assert_eq!(tr_context(lang, key, scope), *text, "{lang}: {key} ({scope})");
|
||||
if !text.trim().is_empty() {
|
||||
assert_eq!(tr_context(lang, key, scope), *text, "{lang}: {key} ({scope})");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, scope, row) in CONTEXT {
|
||||
@@ -4841,8 +4850,10 @@ mod tests {
|
||||
.filter(|(code, _)| *code == lang)
|
||||
.flat_map(|(_, table)| table.iter())
|
||||
.find(|(k, s, _)| k == key && s == scope)
|
||||
.map(|(_, _, text)| *text);
|
||||
assert_eq!(tr_context(lang, key, scope), own.unwrap_or(*shared), "{lang}: {key} ({scope})");
|
||||
.map(|(_, _, text)| *text)
|
||||
.filter(|text| !text.trim().is_empty());
|
||||
let expected = own.or(Some(*shared).filter(|text| !text.trim().is_empty())).unwrap_or_else(|| tr(lang, key));
|
||||
assert_eq!(tr_context(lang, key, scope), expected, "{lang}: {key} ({scope})");
|
||||
}
|
||||
}
|
||||
// An unknown context, or a language without one, keeps the plain translation.
|
||||
@@ -4855,33 +4866,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every catalog: no duplicate or stale keys, no empty translations, and placeholders, padding,
|
||||
/// ellipses and label colons as in English. A string a catalog doesn't translate yet falls back
|
||||
/// to English, so missing translations don't fail here.
|
||||
#[test]
|
||||
fn every_catalog_is_unique_current_and_keeps_placeholders_and_markers() {
|
||||
// (language, key, translation) of every catalog, and every catalog's keys by name.
|
||||
let mut entries: Vec<(&str, &str, &str)> = Vec::new();
|
||||
let mut catalogs: Vec<(&str, HashSet<&str>)> = Vec::new();
|
||||
for (lang, table) in LANGUAGE_TABLES {
|
||||
let keys: HashSet<_> = table.iter().map(|(key, _)| *key).collect();
|
||||
assert_eq!(keys.len(), table.len(), "{lang}: duplicate keys");
|
||||
catalogs.push((*lang, keys));
|
||||
entries.extend(table.iter().map(|(key, text)| (*lang, *key, *text)));
|
||||
}
|
||||
let shared: HashSet<_> = TABLE.iter().map(|(key, _)| *key).collect();
|
||||
assert_eq!(shared.len(), TABLE.len(), "duplicate keys in the shared table");
|
||||
catalogs.push(("shared", shared.clone()));
|
||||
/// The tables `tr` reads, as (language, key, translation), context captions included.
|
||||
fn entries() -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut entries: Vec<_> = LANGUAGE_TABLES.iter().flat_map(|(lang, table)| table.iter().map(|(key, text)| (*lang, *key, *text))).collect();
|
||||
for (lang, i) in SHARED_COLUMNS.into_iter().zip(0..) {
|
||||
entries.extend(TABLE.iter().filter_map(|(key, row)| row.get(i).map(|text| (lang, *key, *text))));
|
||||
entries.extend(CONTEXT.iter().filter_map(|(key, _, row)| row.get(i).map(|text| (lang, *key, *text))));
|
||||
}
|
||||
let mut contexts = vec![("shared", CONTEXT.iter().map(|(key, scope, _)| (*key, *scope)).collect::<Vec<_>>())];
|
||||
for (lang, table) in CONTEXT_TABLES {
|
||||
contexts.push((*lang, table.iter().map(|(key, scope, _)| (*key, *scope)).collect()));
|
||||
entries.extend(table.iter().map(|(key, _, text)| (*lang, *key, *text)));
|
||||
}
|
||||
entries.extend(CONTEXT_TABLES.iter().flat_map(|(lang, table)| table.iter().map(|(key, _, text)| (*lang, *key, *text))));
|
||||
entries
|
||||
}
|
||||
|
||||
/// Every catalog has one row per key and keeps the English placeholders. A string a catalog
|
||||
/// doesn't translate shows in English, so a missing or empty row fails nothing here; the
|
||||
/// `missing_translations_report` lists them.
|
||||
#[test]
|
||||
fn every_catalog_has_unique_keys_and_keeps_placeholders() {
|
||||
for (lang, i) in SHARED_COLUMNS.iter().zip(0..) {
|
||||
assert_eq!(column(lang), Some(i), "{lang}");
|
||||
}
|
||||
for (lang, table) in LANGUAGE_TABLES {
|
||||
let keys: HashSet<_> = table.iter().map(|(key, _)| *key).collect();
|
||||
assert_eq!(keys.len(), table.len(), "{lang}: duplicate keys");
|
||||
}
|
||||
let shared: HashSet<_> = TABLE.iter().map(|(key, _)| *key).collect();
|
||||
assert_eq!(shared.len(), TABLE.len(), "duplicate keys in the shared table");
|
||||
let shared_contexts: HashSet<_> = CONTEXT.iter().map(|(key, scope, _)| (*key, *scope)).collect();
|
||||
assert_eq!(shared_contexts.len(), CONTEXT.len(), "duplicate keys in the shared context table");
|
||||
for (lang, table) in CONTEXT_TABLES {
|
||||
let keys: HashSet<_> = table.iter().map(|(key, scope, _)| (*key, *scope)).collect();
|
||||
assert_eq!(keys.len(), table.len(), "{lang}: duplicate context keys");
|
||||
}
|
||||
// A per-language table is looked up before the shared table, so a key in both hides the
|
||||
// shared column, except in the tables that translate the whole interface.
|
||||
for (lang, table) in LANGUAGE_TABLES.iter().filter(|(lang, _)| column(lang).is_some() && !WHOLE_INTERFACE.contains(lang)) {
|
||||
@@ -4889,41 +4904,58 @@ mod tests {
|
||||
assert!(both.is_empty(), "{lang} keys also in the shared table: {both:?}");
|
||||
}
|
||||
|
||||
// A key is current while the interface or another catalog has it.
|
||||
let interface = interface_strings();
|
||||
let current = |key: &str, own: &str| interface.contains(key) || catalogs.iter().any(|(name, keys)| *name != own && keys.contains(key));
|
||||
for (name, keys) in &catalogs {
|
||||
let mut stale: Vec<_> = keys.iter().filter(|key| !current(key, name)).collect();
|
||||
stale.sort();
|
||||
assert!(stale.is_empty(), "{name}: entries for unknown keys: {stale:?}");
|
||||
}
|
||||
for (name, keys) in &contexts {
|
||||
let unique: HashSet<_> = keys.iter().collect();
|
||||
assert_eq!(unique.len(), keys.len(), "{name}: duplicate context keys");
|
||||
let stale: Vec<_> = keys.iter().filter(|(key, _)| !current(key, "")).collect();
|
||||
assert!(stale.is_empty(), "{name}: contexts for unknown keys: {stale:?}");
|
||||
}
|
||||
|
||||
let placeholders = |s: &'static str| {
|
||||
let mut names: Vec<_> = s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name)).collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
};
|
||||
let ellipsis = |s: &str| s.ends_with('…') || s.ends_with("...");
|
||||
let colon = |s: &str| s.ends_with(':') || s.ends_with(':');
|
||||
// Full-width punctuation carries its own spacing.
|
||||
let full_width = |c: char| ('\u{3000}'..='\u{303F}').contains(&c) || ('\u{FF00}'..='\u{FFEF}').contains(&c);
|
||||
for (lang, key, text) in entries {
|
||||
assert!(!text.trim().is_empty(), "{lang}: empty translation of {key:?}");
|
||||
for (lang, key, text) in entries().into_iter().filter(|(_, _, text)| !text.trim().is_empty()) {
|
||||
assert_eq!(placeholders(key), placeholders(text), "{lang}: placeholders of {key:?}");
|
||||
// Dialog ellipses, label colons and padded captions stay as in English.
|
||||
assert_eq!(ellipsis(key), ellipsis(text), "{lang}: ellipsis of {key:?}");
|
||||
assert_eq!(colon(key), colon(text), "{lang}: colon of {key:?}");
|
||||
if !text.starts_with(full_width) {
|
||||
assert_eq!(key.starts_with(' '), text.starts_with(' '), "{lang}: leading space of {key:?}");
|
||||
}
|
||||
if !text.ends_with(full_width) {
|
||||
assert_eq!(key.ends_with(' '), text.ends_with(' '), "{lang}: trailing space of {key:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The strings each interface language shows in English, for the `cargo xtask ci` log. It
|
||||
/// always passes: `cargo xtask i18n-report` runs it.
|
||||
#[test]
|
||||
#[ignore = "a report, run by `cargo xtask i18n-report`"]
|
||||
fn missing_translations_report() {
|
||||
let interface = interface_strings();
|
||||
let catalogs: Vec<(&str, HashSet<&str>)> = LANGUAGE_TABLES
|
||||
.iter()
|
||||
.map(|(lang, table)| (*lang, table.iter().map(|(key, _)| *key).collect()))
|
||||
.chain([("shared", TABLE.iter().map(|(key, _)| *key).collect())])
|
||||
.collect();
|
||||
let mut keys: Vec<&str> = interface.iter().copied().chain(catalogs.iter().flat_map(|(_, keys)| keys.iter().copied())).collect();
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
// `tr` hands back the string it was given when it has no translation.
|
||||
let missing = |lang: &str| -> Vec<&str> {
|
||||
keys.iter()
|
||||
.copied()
|
||||
.filter(|key| {
|
||||
let english = (*key).to_owned();
|
||||
std::ptr::eq(tr(lang, &english), english.as_str())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let report: Vec<(&str, Vec<&str>)> = LANGUAGES.iter().filter(|(code, _)| !code.is_empty()).map(|(code, _)| (*code, missing(code))).collect();
|
||||
println!("i18n report: {} interface strings; a missing translation shows in English.", keys.len());
|
||||
for (lang, missing) in &report {
|
||||
println!(" {lang:<6} {:>5} missing", missing.len());
|
||||
}
|
||||
for (lang, missing) in report.iter().filter(|(_, missing)| !missing.is_empty()) {
|
||||
println!("{lang} ({}): {}", missing.len(), missing.iter().map(|key| format!("{key:?}")).collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
// A catalog key neither the interface list nor another catalog knows is stale, or a label
|
||||
// `interface_strings` doesn't list yet.
|
||||
for (name, own) in &catalogs {
|
||||
let mut unknown: Vec<_> = own
|
||||
.iter()
|
||||
.filter(|key| !interface.contains(*key) && !catalogs.iter().any(|(other, keys)| other != name && keys.contains(*key)))
|
||||
.collect();
|
||||
unknown.sort_unstable();
|
||||
if !unknown.is_empty() {
|
||||
println!("{name}: {} rows for keys the interface list doesn't have: {unknown:?}", unknown.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4951,47 +4983,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Languages with several plural forms put the category first, so every number reads the same.
|
||||
#[test]
|
||||
fn georgian_counts_use_category_captions_for_every_number() {
|
||||
for n in [1, 2, 5, 21] {
|
||||
assert_eq!(count_label("ka", "pages", n), format!("გვერდი: {n}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_counts_use_category_captions_for_every_number() {
|
||||
for n in [0, 1, 2, 4, 5, 11, 21, 22, 25] {
|
||||
assert_eq!(count_label("uk", "pages", n), format!("сторінок: {n}"));
|
||||
assert_eq!(count_label("uk", "objects", n), format!("об’єктів: {n}"));
|
||||
assert_eq!(count_label("uk", "errors", n), format!("помилок: {n}"));
|
||||
let pages = tr("uk", "{pages} Pages in {spreads} Spreads").replace("{pages}", &n.to_string()).replace("{spreads}", &n.to_string());
|
||||
assert_eq!(pages, format!("Сторінок: {n}; розворотів: {n}"));
|
||||
fn category_first_counts_put_the_category_before_every_number() {
|
||||
assert!(!category_first("de") && !category_first(""));
|
||||
for lang in ["ka", "uk", "ru", "pl"] {
|
||||
assert!(category_first(lang), "{lang}");
|
||||
for noun in ["pages", "objects", "errors"] {
|
||||
for n in [0, 1, 2, 4, 5, 11, 12, 21, 22, 25, 101, 111] {
|
||||
assert_eq!(count_label(lang, noun, n), format!("{}: {n}", tr(lang, noun)), "{lang}: {noun}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(count_label("", "objects", 2), "2 objects");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_counts_use_category_captions_for_every_number() {
|
||||
assert!(category_first("ru") && category_first("uk") && !category_first("de") && !category_first(""));
|
||||
for n in [0, 1, 2, 4, 5, 11, 12, 21, 22, 25, 101, 111] {
|
||||
assert_eq!(count_label("ru", "pages", n), format!("страниц: {n}"));
|
||||
assert_eq!(count_label("ru", "objects", n), format!("объектов: {n}"));
|
||||
assert_eq!(count_label("ru", "errors", n), format!("ошибок: {n}"));
|
||||
let pages = tr("ru", "{pages} Pages in {spreads} Spreads").replace("{pages}", &n.to_string()).replace("{spreads}", &n.to_string());
|
||||
assert_eq!(pages, format!("Страниц: {n}; разворотов: {n}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polish_counts_use_category_captions_for_every_number() {
|
||||
for n in [0, 1, 2, 5, 12, 22, 25] {
|
||||
assert_eq!(count_label("pl", "pages", n), format!("Strony: {n}"));
|
||||
assert_eq!(count_label("pl", "objects", n), format!("Obiekty: {n}"));
|
||||
assert_eq!(count_label("pl", "errors", n), format!("Błędy: {n}"));
|
||||
}
|
||||
assert_eq!(tr("pl", "{pages} Pages in {spreads} Spreads"), "Strony: {pages}, rozkładówki: {spreads}");
|
||||
}
|
||||
|
||||
/// Every English string the interface passes to `tr`, as far as the command and tool tables and
|
||||
/// the list of UI-only labels know them.
|
||||
fn interface_strings() -> HashSet<&'static str> {
|
||||
@@ -5092,26 +5098,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn czech_counts_use_the_plural_form_for_the_number() {
|
||||
for (n, form) in [(0, "chyb"), (1, "chyba"), (2, "chyby"), (4, "chyby"), (5, "chyb"), (11, "chyb"), (22, "chyb")] {
|
||||
assert_eq!(count_label("cs", "errors", n), format!("{n} {form}"));
|
||||
for (noun, [one, few, other]) in cs::PLURALS {
|
||||
for (n, form) in [(0, other), (1, one), (2, few), (4, few), (5, other), (11, other), (22, other)] {
|
||||
let expected = if form.trim().is_empty() { format!("{n} {}", tr("cs", noun)) } else { format!("{n} {form}") };
|
||||
assert_eq!(count_label("cs", noun, n), expected, "{noun}");
|
||||
}
|
||||
}
|
||||
// The status bar passes the singular noun for one error.
|
||||
assert_eq!(count_label("cs", "error", 1), "1 chyba");
|
||||
assert_eq!(count_label("cs", "pages", 1), "1 stránka");
|
||||
assert_eq!(count_label("cs", "pages", 3), "3 stránky");
|
||||
assert_eq!(count_label("cs", "pages", 5), "5 stránek");
|
||||
assert_eq!(count_label("cs", "stories", 2), "2 články");
|
||||
assert_eq!(count_label("cs", "items", 1), "1 položka");
|
||||
assert_eq!(count_label("cs", "objects", 7), "7 objektů");
|
||||
assert_eq!(count_label("cs", "warnings", 3), "3 varování");
|
||||
assert_eq!(count_label("cs", "layer", 1), "1 vrstva");
|
||||
assert_eq!(count_label("cs", "layers", 3), "3 vrstvy");
|
||||
assert_eq!(count_label("cs", "layers", 8), "8 vrstev");
|
||||
// A noun without plural forms keeps the plain translation.
|
||||
assert_eq!(count_label("cs", "a new noun", 3), "3 a new noun");
|
||||
for (noun, forms) in cs::PLURALS {
|
||||
assert!(forms.iter().all(|form| !form.is_empty()), "{noun}");
|
||||
assert!(forms.contains(&tr("cs", noun)), "{noun}: the catalog row is one of the plural forms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2098,55 +2098,3 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Update Data Source", "Datenquelle aktualisieren"),
|
||||
("Validate from Root Element", "Ab Stammelement prüfen"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::TABLE;
|
||||
use crate::i18n::tr;
|
||||
|
||||
fn placeholders(text: &str) -> Vec<&str> {
|
||||
let mut found = Vec::new();
|
||||
let mut rest = text;
|
||||
while let Some(start) = rest.find('{') {
|
||||
let Some(len) = rest[start..].find('}') else { break };
|
||||
found.push(&rest[start..start + len + 1]);
|
||||
rest = &rest[start + len + 1..];
|
||||
}
|
||||
found.sort_unstable();
|
||||
found
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_are_unique_and_complete() {
|
||||
let mut seen = HashSet::new();
|
||||
for (en, de) in TABLE {
|
||||
assert!(seen.insert(*en), "duplicate {en}");
|
||||
assert!(!de.is_empty(), "{en}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_keep_placeholders_padding_and_endings() {
|
||||
let ellipsis = |text: &str| text.trim_end().ends_with('…') || text.trim_end().ends_with("...");
|
||||
for (en, de) in TABLE {
|
||||
assert_eq!(placeholders(en), placeholders(de), "placeholders of {en}");
|
||||
assert_eq!(en.len() - en.trim_start().len(), de.len() - de.trim_start().len(), "leading space of {en}");
|
||||
assert_eq!(en.len() - en.trim_end().len(), de.len() - de.trim_end().len(), "trailing space of {en}");
|
||||
assert_eq!(ellipsis(en), ellipsis(de), "ellipsis of {en}");
|
||||
// The numbering samples (`a, b, c, d...`) stay as they are; everything else uses `…`.
|
||||
assert!(!de.contains("...") || de == en, "ASCII ellipsis in {en}");
|
||||
assert_eq!(en.trim_end().ends_with(':'), de.trim_end().ends_with(':'), "colon of {en}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_uses_this_table_and_leaves_unknown_text_alone() {
|
||||
assert_eq!(tr("de", "File"), "Datei");
|
||||
assert_eq!(tr("de", "Cancel"), "Abbrechen");
|
||||
assert_eq!(tr("de", "{count} glyphs — click to insert at the text cursor").matches("{count}").count(), 1);
|
||||
let unknown = String::from("A user-defined untranslated label");
|
||||
assert!(std::ptr::eq(tr("de", &unknown), unknown.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2147,55 +2147,3 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
|
||||
/// Captions whose Japanese depends on where they appear: (English, context, Japanese).
|
||||
pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Span Columns", "paragraphLayout", "段抜き"), ("Columns:", "grid", "列:")];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::TABLE;
|
||||
use crate::i18n::tr;
|
||||
|
||||
fn placeholders(text: &str) -> Vec<&str> {
|
||||
let mut found = Vec::new();
|
||||
let mut rest = text;
|
||||
while let Some(start) = rest.find('{') {
|
||||
let Some(len) = rest[start..].find('}') else { break };
|
||||
found.push(&rest[start..start + len + 1]);
|
||||
rest = &rest[start + len + 1..];
|
||||
}
|
||||
found.sort_unstable();
|
||||
found
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_are_unique_and_complete() {
|
||||
let mut seen = HashSet::new();
|
||||
for (en, ja) in TABLE {
|
||||
assert!(seen.insert(*en), "duplicate {en}");
|
||||
assert!(!ja.is_empty(), "{en}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_keep_placeholders_padding_and_endings() {
|
||||
let ellipsis = |text: &str| text.trim_end().ends_with('…') || text.trim_end().ends_with("...");
|
||||
for (en, ja) in TABLE {
|
||||
assert_eq!(placeholders(en), placeholders(ja), "placeholders of {en}");
|
||||
assert_eq!(en.len() - en.trim_start().len(), ja.len() - ja.trim_start().len(), "leading space of {en}");
|
||||
assert_eq!(en.len() - en.trim_end().len(), ja.len() - ja.trim_end().len(), "trailing space of {en}");
|
||||
assert_eq!(ellipsis(en), ellipsis(ja), "ellipsis of {en}");
|
||||
// The numbering samples (`a, b, c, d...`) stay as they are; everything else uses `…`.
|
||||
assert!(!ja.contains("...") || ja == en, "ASCII ellipsis in {en}");
|
||||
assert_eq!(en.trim_end().ends_with(':'), ja.trim_end().ends_with(':'), "colon of {en}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_uses_this_table_and_leaves_unknown_text_alone() {
|
||||
assert_eq!(tr("ja", "File"), "ファイル");
|
||||
assert_eq!(tr("ja", "Cancel"), "キャンセル");
|
||||
assert_eq!(tr("ja", "{count} glyphs — click to insert at the text cursor").matches("{count}").count(), 1);
|
||||
let unknown = String::from("A user-defined untranslated label");
|
||||
assert!(std::ptr::eq(tr("ja", &unknown), unknown.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native_job_lifetime_control;
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//! Own a real native Job across last close. This does not run the asynchronous worker.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** minor (Polish and Georgian interfaces; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (missing translations are reported, never a failure; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
|
||||
Per-language status of the DesignCraft interface and of document typesetting in each script.
|
||||
Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-typography.md),
|
||||
@@ -19,6 +19,10 @@ in the UI crate appear in no catalog (some are test data or proper names), so th
|
||||
somewhat higher and every percentage below is a slight overestimate. Help: DesignCraft has no
|
||||
help content in any language (Help ▸ opens the project's pages).
|
||||
|
||||
A missing translation never fails a test, a build or a release. `cargo xtask i18n-report` (also
|
||||
a step of `cargo xtask ci`, so the list is in every gate log) prints how many interface strings
|
||||
each language lacks, then the strings.
|
||||
|
||||
InDesign 2026 ships 25 interface localizations (`.lproj` folders: cs, da, de, el, en, en_GB, es,
|
||||
fi, fr, hu, it, ja, ko, nb, nl, pl, pt, ro, ru, sq, sv, tr, uk, zh_CN, zh_TW); Arabic and Hebrew
|
||||
come as Middle East editions with an English or French interface and right-to-left features.
|
||||
|
||||
+12
-7
@@ -19,10 +19,11 @@ workspace_version() {
|
||||
/^\[/ { in_pkg = ($0 == "[workspace.package]") ; next }
|
||||
in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit }
|
||||
' "$ROOT/Cargo.toml"
|
||||
return
|
||||
}
|
||||
|
||||
VERSION="${DESIGNCRAFT_VERSION:-$(workspace_version)}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -32,7 +33,7 @@ DIST="${DIST:-$ROOT/dist/release}"
|
||||
mkdir -p "$DIST"
|
||||
export DIST
|
||||
|
||||
if [ -z "${DESIGNCRAFT_BUILD_SHA:-}" ]; then
|
||||
if [[ -z "${DESIGNCRAFT_BUILD_SHA:-}" ]]; then
|
||||
DESIGNCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
|
||||
fi
|
||||
export DESIGNCRAFT_BUILD_SHA
|
||||
@@ -41,29 +42,33 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
|
||||
|
||||
# Emit a GitHub Actions warning (plain stderr outside Actions).
|
||||
warn() {
|
||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
return
|
||||
}
|
||||
|
||||
# Copy licence and readme files that exist into a package directory.
|
||||
copy_docs() {
|
||||
local dest="$1" f
|
||||
for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do
|
||||
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
if [[ -f "$ROOT/$f" ]]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
done
|
||||
copy_font_licences "$dest"
|
||||
return
|
||||
}
|
||||
|
||||
# Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so
|
||||
# the package carries their licences as OFL-<family-dir>.txt. Nothing without CRAFT_FONTS_DIR.
|
||||
copy_font_licences() {
|
||||
local dest="$1" f
|
||||
[ -n "${CRAFT_FONTS_DIR:-}" ] || return 0
|
||||
[[ -n "${CRAFT_FONTS_DIR:-}" ]] || return 0
|
||||
for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do
|
||||
if [ -f "$f" ]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi
|
||||
if [[ -f "$f" ]]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi
|
||||
done
|
||||
}
|
||||
|
||||
# Portable SHA-256 of a file (prints just the hash).
|
||||
sha256() {
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$file" | cut -d' ' -f1; else shasum -a 256 "$file" | cut -d' ' -f1; fi
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ APP_ID=ai.storyteller.designcraft
|
||||
|
||||
SKIP_BUILD=0
|
||||
DRY_RUN=0
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; SKIP_BUILD=1; shift ;;
|
||||
@@ -36,7 +36,7 @@ case "$MACHINE" in
|
||||
arm64 | aarch64) ARCH=aarch64 ;;
|
||||
*) echo "unsupported architecture $MACHINE" >&2; exit 2 ;;
|
||||
esac
|
||||
if [ "$DRY_RUN" = 0 ] && [ "$(uname -s)" != FreeBSD ]; then
|
||||
if [[ "$DRY_RUN" = 0 ]] && [[ "$(uname -s)" != FreeBSD ]]; then
|
||||
echo "error: build on FreeBSD (or pass --dry-run to check the tree layout)" >&2
|
||||
exit 2
|
||||
fi
|
||||
@@ -46,7 +46,7 @@ echo "==> DesignCraft $VERSION for FreeBSD $ARCH"
|
||||
|
||||
# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory.
|
||||
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}"
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli)
|
||||
fi
|
||||
WORK="$CARGO_TARGET_DIR/freebsd-package"
|
||||
@@ -56,7 +56,7 @@ mkdir -p "$WORK"
|
||||
|
||||
BIN="$CARGO_TARGET_DIR/release"
|
||||
OUT_DIR="$DIST"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
if [[ "$DRY_RUN" = 1 ]]; then
|
||||
# Stub binaries, and the tarball stays in the work dir so dist/ only ever holds real builds.
|
||||
BIN="$WORK/stub-bin"
|
||||
OUT_DIR="$WORK"
|
||||
@@ -81,13 +81,13 @@ sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$DESIGNCRAFT_BUILD_DATE/g" \
|
||||
cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/"
|
||||
copy_docs "$STAGE/share/doc/designcraft"
|
||||
for f in NOTICE ATTRIBUTION.md; do
|
||||
if [ -f "$ROOT/$f" ]; then install -m 644 "$ROOT/$f" "$STAGE/share/doc/designcraft/"; fi
|
||||
if [[ -f "$ROOT/$f" ]]; then install -m 644 "$ROOT/$f" "$STAGE/share/doc/designcraft/"; fi
|
||||
done
|
||||
|
||||
for f in bin/designcraft bin/designcraft-cli "share/applications/$APP_ID.desktop" \
|
||||
"share/icons/hicolor/256x256/apps/$APP_ID.png" "share/icons/hicolor/scalable/apps/$APP_ID.svg" \
|
||||
share/doc/designcraft/LICENSE-MIT share/doc/designcraft/LICENSE-APACHE; do
|
||||
if [ ! -e "$STAGE/$f" ]; then echo "error: $f is missing from the package" >&2; exit 1; fi
|
||||
if [[ ! -e "$STAGE/$f" ]]; then echo "error: $f is missing from the package" >&2; exit 1; fi
|
||||
done
|
||||
|
||||
# ---- .tar.gz ------------------------------------------------------------------------------------
|
||||
@@ -96,7 +96,7 @@ tar -C "$WORK" -czf "$OUT_DIR/$BASENAME.tar.gz" "$BASENAME"
|
||||
echo "wrote $OUT_DIR/$BASENAME.tar.gz"
|
||||
|
||||
"$STAGE/bin/designcraft-cli" --version
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
if [[ "$DRY_RUN" = 1 ]]; then
|
||||
echo "==> dry run: tarball contents"
|
||||
tar -tzvf "$OUT_DIR/$BASENAME.tar.gz"
|
||||
fi
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ MAC="$TMP/mac.svg"
|
||||
sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC"
|
||||
grep -q 'viewBox="-62.136' "$MAC" || { echo "error: expected viewBox=\"0 0 512 512\" in $SVG" >&2; exit 1; }
|
||||
|
||||
render() { resvg -w "$2" -h "$2" "$1" "$3" </dev/null; }
|
||||
render() {
|
||||
local svg="$1" size="$2" out="$3"
|
||||
resvg -w "$size" -h "$size" "$svg" "$out" </dev/null
|
||||
}
|
||||
|
||||
render "$SVG" 1024 "$DIR/designcraft-1024.png"
|
||||
# Runtime window/Dock icon on macOS (embedded by apps/designcraft); other platforms use hicolor 256.
|
||||
|
||||
@@ -21,7 +21,7 @@ FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
TEST=1
|
||||
TARBALL=""
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-test) TEST=0; shift ;;
|
||||
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||
@@ -38,7 +38,7 @@ case "$ARCH" in
|
||||
esac
|
||||
BASENAME="designcraft-$VERSION-linux-$ARCH"
|
||||
TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}"
|
||||
[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
for tool in flatpak flatpak-builder; do
|
||||
command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; }
|
||||
done
|
||||
@@ -49,7 +49,7 @@ WORK="$CARGO_TARGET_DIR/flatpak-bundle"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK/stage"
|
||||
tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1
|
||||
[ -x "$WORK/stage/bin/designcraft" ] || { echo "error: $TARBALL has no bin/designcraft" >&2; exit 1; }
|
||||
[[ -x "$WORK/stage/bin/designcraft" ]] || { echo "error: $TARBALL has no bin/designcraft" >&2; exit 1; }
|
||||
cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml"
|
||||
|
||||
flatpak remote-add --user --if-not-exists flathub "$FLATHUB"
|
||||
@@ -62,7 +62,7 @@ OUT="$DIST/$BASENAME.flatpak"
|
||||
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
|
||||
echo "wrote $OUT"
|
||||
|
||||
if [ "$TEST" = 1 ]; then
|
||||
if [[ "$TEST" = 1 ]]; then
|
||||
flatpak install --user -y --noninteractive --reinstall "$OUT"
|
||||
flatpak info --user "$APP_ID"
|
||||
flatpak run --command=designcraft-cli "$APP_ID" --version
|
||||
|
||||
@@ -22,7 +22,7 @@ APP_ID=ai.storyteller.designcraft
|
||||
|
||||
SKIP_BUILD=0
|
||||
FORMATS="appimage deb rpm arch tar"
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--formats) FORMATS="$2"; shift 2 ;;
|
||||
@@ -43,8 +43,8 @@ BASENAME="designcraft-$VERSION-linux-$ARCH"
|
||||
|
||||
echo "==> DesignCraft $VERSION for Linux $ARCH ($FORMATS)"
|
||||
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [ -n "${CROSS_TARGET:-}" ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
if [[ -n "${CROSS_TARGET:-}" ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli --target "$CROSS_TARGET")
|
||||
else
|
||||
(cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli)
|
||||
@@ -76,7 +76,10 @@ if command -v appstreamcli >/dev/null; then
|
||||
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
|
||||
fi
|
||||
|
||||
has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
has() {
|
||||
local fmt="$1"
|
||||
case " $FORMATS " in *" $fmt "*) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
# ---- .tar.gz ------------------------------------------------------------------------------------
|
||||
if has tar; then
|
||||
@@ -134,6 +137,8 @@ if has appimage; then
|
||||
case "$ARCH" in
|
||||
x86_64) APPIMAGETOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 ;;
|
||||
aarch64) APPIMAGETOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 ;;
|
||||
# No pinned download (e.g. riscv64): set APPIMAGETOOL or put appimagetool on PATH.
|
||||
*) APPIMAGETOOL_SHA256="" ;;
|
||||
esac
|
||||
APPDIR="$WORK/DesignCraft.AppDir"
|
||||
cp -R "$STAGE" "$APPDIR"
|
||||
@@ -144,9 +149,9 @@ if has appimage; then
|
||||
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
|
||||
|
||||
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
||||
if [ -z "$TOOL" ]; then
|
||||
if [[ -z "$TOOL" ]]; then
|
||||
TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage"
|
||||
if [ ! -x "$TOOL" ]; then
|
||||
if [[ ! -x "$TOOL" ]]; then
|
||||
download="$TOOL.download"
|
||||
curl --proto '=https' -fsSL -o "$download" \
|
||||
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
||||
@@ -174,16 +179,16 @@ if has appimage; then
|
||||
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
||||
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
||||
echo "wrote $OUT"
|
||||
if [ -s "$OUT.zsync" ]; then
|
||||
if [[ -s "$OUT.zsync" ]]; then
|
||||
echo "wrote $OUT.zsync"
|
||||
else
|
||||
warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${CROSS_TARGET:-}" ]; then
|
||||
if [[ -z "${CROSS_TARGET:-}" ]]; then
|
||||
"$STAGE/usr/bin/designcraft-cli" --version
|
||||
elif [ -n "${EMULATOR:-}" ]; then
|
||||
elif [[ -n "${EMULATOR:-}" ]]; then
|
||||
"$EMULATOR" "$STAGE/usr/bin/designcraft-cli" --version
|
||||
fi
|
||||
echo "==> done"
|
||||
|
||||
@@ -12,7 +12,7 @@ umask 077
|
||||
# shellcheck source=../env.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
|
||||
warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized"
|
||||
exit 0
|
||||
fi
|
||||
@@ -36,13 +36,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS"
|
||||
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
|
||||
|
||||
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')"
|
||||
if [ -z "$IDENTITY" ]; then
|
||||
if [[ -z "$IDENTITY" ]]; then
|
||||
security find-identity -v -p codesigning "$KEYCHAIN" >&2
|
||||
echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Imported signing identity $IDENTITY"
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
{ echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV"
|
||||
else
|
||||
echo "MACOS_SIGN_IDENTITY=$IDENTITY"
|
||||
|
||||
+12
-12
@@ -19,7 +19,7 @@ HERE="$ROOT/packaging/macos"
|
||||
|
||||
ARCH=universal
|
||||
SKIP_BUILD=0
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
@@ -44,9 +44,9 @@ DMG="$DIST/designcraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/designcraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
|
||||
NOTARIZE=0
|
||||
if [ "$IDENTITY" = "-" ]; then
|
||||
if [[ "$IDENTITY" = "-" ]]; then
|
||||
warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized"
|
||||
elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
elif [[ -n "${APPLE_ID:-}" ]] && [[ -n "${APPLE_PASSWORD:-}" ]] && [[ -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
NOTARIZE=1
|
||||
else
|
||||
warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized"
|
||||
@@ -55,7 +55,7 @@ fi
|
||||
echo "==> DesignCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE"
|
||||
|
||||
# ---- build -------------------------------------------------------------------------------------
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
args=()
|
||||
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
||||
(cd "$ROOT" && cargo build --release --locked -p designcraft -p designcraft-cli "${args[@]}")
|
||||
@@ -74,9 +74,9 @@ done
|
||||
sign() {
|
||||
# Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped.
|
||||
local ts=(--timestamp)
|
||||
[ "$IDENTITY" = "-" ] && ts=(--timestamp=none)
|
||||
[[ "$IDENTITY" = "-" ]] && ts=(--timestamp=none)
|
||||
local kc=()
|
||||
[ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
[[ -n "${MACOS_KEYCHAIN:-}" ]] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@"
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ notarize() {
|
||||
echo "$out"
|
||||
id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)"
|
||||
status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)"
|
||||
if [ "$status" != "Accepted" ]; then
|
||||
if [ -n "$id" ]; then
|
||||
if [[ "$status" != "Accepted" ]]; then
|
||||
if [[ -n "$id" ]]; then
|
||||
xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true
|
||||
fi
|
||||
echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2
|
||||
@@ -117,7 +117,7 @@ sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/
|
||||
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP"
|
||||
codesign --verify --strict --deep --verbose=2 "$APP"
|
||||
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
ditto -c -k --keepParent "$APP" "$WORK/DesignCraft-notarize.zip"
|
||||
notarize "$WORK/DesignCraft-notarize.zip"
|
||||
xcrun stapler staple "$APP"
|
||||
@@ -151,7 +151,7 @@ hdiutil_retry() {
|
||||
if hdiutil "$@"; then
|
||||
return 0
|
||||
fi
|
||||
[ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; }
|
||||
[[ "$attempt" = 5 ]] && { echo "hdiutil $what failed 5 times" >&2; return 1; }
|
||||
echo "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" >&2
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
@@ -169,7 +169,7 @@ if ! codesign --verify --strict --deep --verbose=2 "$MOUNT/DesignCraft.app"; the
|
||||
exit 1
|
||||
fi
|
||||
hdiutil_retry detach detach "$MOUNT" >/dev/null
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
notarize "$DMG"
|
||||
xcrun stapler staple "$DMG"
|
||||
xcrun stapler validate "$DMG"
|
||||
@@ -187,7 +187,7 @@ codesign --verify --strict --verbose=2 "$CLI_DIR/designcraft-cli"
|
||||
rm -f "$CLI_ZIP"
|
||||
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
|
||||
# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online.
|
||||
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
|
||||
if [[ "$NOTARIZE" = 1 ]]; then notarize "$CLI_ZIP"; fi
|
||||
|
||||
"$WORK/bin/designcraft-cli" --version
|
||||
echo "==> done"
|
||||
|
||||
@@ -11,13 +11,13 @@ set -euo pipefail
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
HERE="$ROOT/packaging/web"
|
||||
|
||||
if [ "${1:-}" != "--skip-build" ]; then
|
||||
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||
command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; }
|
||||
(cd "$ROOT/apps/designcraft-web" && trunk build --release)
|
||||
fi
|
||||
|
||||
SITE="$ROOT/dist/web"
|
||||
[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
[[ -f "$SITE/index.html" ]] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
# Paths must be relative so the site works under any prefix (public_url = "./" in Trunk.toml).
|
||||
if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then
|
||||
echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2
|
||||
|
||||
+66
-21
@@ -12,7 +12,7 @@ mod stats;
|
||||
mod version;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode};
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
|
||||
const USAGE: &str = "\
|
||||
usage: cargo xtask <command>
|
||||
@@ -21,8 +21,11 @@ commands:
|
||||
assets check that every icon/image/font/asset is attributed in ASSETS.md
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
pdf-deps verify the pinned PDF backend, including Krilla dependencies
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
|
||||
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates (one call)
|
||||
test workspace tests (cargo nextest when installed, else cargo test), then doctests
|
||||
i18n-report list the interface strings each language shows in English (always passes)
|
||||
ci fmt --check, pdf-deps, clippy -D warnings, test, i18n-report, doctest, assets,
|
||||
layers, wasm (stops at first failure)
|
||||
corpus [--download]
|
||||
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
|
||||
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
||||
@@ -39,6 +42,8 @@ fn main() -> ExitCode {
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("pdf-deps") => pdf_deps::run(&root()),
|
||||
Some("wasm") => cmd_wasm(),
|
||||
Some("test") => cmd_test().and_then(|()| cmd_doctest()),
|
||||
Some("i18n-report") => cmd_i18n_report(),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
|
||||
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
|
||||
@@ -134,21 +139,66 @@ fn wasm_set() -> Result<Vec<String>, String> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// One `cargo check` for the whole set: each crate is checked once, and
|
||||
/// `--keep-going` still reports every failing crate. Features unify across the
|
||||
/// set, so each crate is checked with the union of the features the set enables.
|
||||
fn cmd_wasm() -> Result<(), String> {
|
||||
let set = wasm_set()?;
|
||||
let mut results = Vec::new();
|
||||
if set.is_empty() {
|
||||
return Err("no wasm-safe crates found in the workspace metadata".into());
|
||||
}
|
||||
let mut c = cargo();
|
||||
c.args(["check", "--target", "wasm32-unknown-unknown", "--keep-going"]);
|
||||
for pkg in &set {
|
||||
let mut c = cargo();
|
||||
c.args(["check", "--target", "wasm32-unknown-unknown", "-p", pkg]);
|
||||
let ok = run(c, &format!("cargo check --target wasm32-unknown-unknown -p {pkg}")).is_ok();
|
||||
results.push((pkg.clone(), ok));
|
||||
c.args(["-p", pkg]);
|
||||
}
|
||||
println!("\nwasm32-unknown-unknown check:");
|
||||
for (p, ok) in &results {
|
||||
println!(" {:<6} {p}", if *ok { "ok" } else { "FAIL" });
|
||||
run(c, &format!("cargo check --target wasm32-unknown-unknown --keep-going -p {}", set.join(" -p ")))?;
|
||||
println!("\nwasm32-unknown-unknown check: ok ({} crates: {})", set.len(), set.join(", "));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test targets for the test build. Examples are left out so they aren't
|
||||
/// linked; clippy `--all-targets` still compiles them.
|
||||
const TEST_TARGETS: [&str; 4] = ["--workspace", "--lib", "--bins", "--tests"];
|
||||
|
||||
fn nextest_available() -> bool {
|
||||
cargo().args(["nextest", "--version"]).stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success())
|
||||
}
|
||||
|
||||
fn cmd_test() -> Result<(), String> {
|
||||
let mut c = cargo();
|
||||
let tool = if nextest_available() {
|
||||
c.args(["nextest", "run"]);
|
||||
"nextest run"
|
||||
} else {
|
||||
eprintln!("note: cargo-nextest is not installed, running cargo test (cargo install cargo-nextest --locked)");
|
||||
c.arg("test");
|
||||
"test"
|
||||
};
|
||||
c.args(TEST_TARGETS);
|
||||
run(c, &format!("cargo {tool} {}", TEST_TARGETS.join(" ")))
|
||||
}
|
||||
|
||||
/// Runs the ignored `missing_translations_report` test with its output shown. It takes the test
|
||||
/// step's targets, so it reuses that build.
|
||||
fn cmd_i18n_report() -> Result<(), String> {
|
||||
const REPORT: &str = "i18n::tests::missing_translations_report";
|
||||
let mut c = cargo();
|
||||
if nextest_available() {
|
||||
c.args(["nextest", "run", "--cargo-quiet"]).args(TEST_TARGETS);
|
||||
c.args(["--run-ignored", "only", "--no-capture", "--status-level", "fail", "--final-status-level", "fail", "-E"]);
|
||||
c.arg(format!("package(designcraft-ui-egui) & test(={REPORT})"));
|
||||
} else {
|
||||
c.args(["test", "-q"]).args(TEST_TARGETS).args(["--", "--ignored", "--exact", REPORT, "--nocapture"]);
|
||||
}
|
||||
let failed = results.iter().filter(|r| !r.1).count();
|
||||
if failed == 0 { Ok(()) } else { Err(format!("{failed} crate(s) failed the wasm check")) }
|
||||
run(c, &format!("missing translations ({REPORT})"))
|
||||
}
|
||||
|
||||
/// nextest does not run doctests.
|
||||
fn cmd_doctest() -> Result<(), String> {
|
||||
let mut c = cargo();
|
||||
c.args(["test", "--workspace", "--doc"]);
|
||||
run(c, "cargo test --workspace --doc")
|
||||
}
|
||||
|
||||
fn cmd_ci() -> Result<(), String> {
|
||||
@@ -171,14 +221,9 @@ fn cmd_ci() -> Result<(), String> {
|
||||
run(c, "cargo clippy --workspace --all-targets -- -D warnings")
|
||||
}),
|
||||
),
|
||||
(
|
||||
"test",
|
||||
Box::new(|| {
|
||||
let mut c = cargo();
|
||||
c.args(["test", "--workspace"]);
|
||||
run(c, "cargo test --workspace")
|
||||
}),
|
||||
),
|
||||
("test", Box::new(cmd_test)),
|
||||
("i18n-report", Box::new(cmd_i18n_report)),
|
||||
("doctest", Box::new(cmd_doctest)),
|
||||
("assets", Box::new(|| assets::run(&root()))),
|
||||
("layers", Box::new(cmd_layers)),
|
||||
("wasm", Box::new(cmd_wasm)),
|
||||
|
||||
Reference in New Issue
Block a user