Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ shift-backends = { path = "crates/shift-backends" }
shift-bridge = { path = "crates/shift-bridge" }
shift-edit = { path = "crates/shift-edit" }
shift-ir = { path = "crates/shift-ir" }
shift-document = { path = "crates/shift-document" }
shift-store = { path = "crates/shift-store" }
12 changes: 12 additions & 0 deletions crates/shift-document/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "shift-document"
version = "0.1.0"
edition = "2024"

[dependencies]
shift-ir = { workspace = true }
shift-store = { workspace = true }
thiserror = "2"

[dev-dependencies]
tempfile = "3"
44 changes: 44 additions & 0 deletions crates/shift-document/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# shift-document

`shift-document` owns the Rust-side workflow for an open Shift document.

A document coordinates the lower-level crates:

- `shift-store` for the SQLite working store;
- `shift-ir` for the live in-memory projection;
- `shift-edit` for interactive editing operations.

Consumers interact with document operations, not raw SQL, raw `Font` mutation, or edit-session internals.

## Responsibilities

- create and open durable Shift documents;
- coordinate writes to the SQLite working store;
- maintain the live `shift-ir` projection used by editing, rendering, and export;
- define where interactive edits become durable document state;
- provide Rust-native operations that bridge, CLI, and future app surfaces can wrap.

## Boundaries

`shift-document` should not contain raw SQL. SQL belongs in `shift-store`.

`shift-document` should not contain TypeScript or NAPI types. Those belong in `shift-bridge` and `shift-wire`.

`shift-document` should not implement low-level vector editing algorithms. Those belong in `shift-edit`.

`shift-document` should not treat SQLite as a pluggable backend. SQLite is the working store for editable Shift documents.

## API Language

Use document-domain language for public APIs:

- `NewDocument`;
- `ShiftDocument`.

Avoid transport, storage-abstraction, or version-control language in document APIs:

- request;
- backend;
- provider;
- store-backed;
- commit session.
43 changes: 43 additions & 0 deletions crates/shift-document/src/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::path::Path;

use crate::{DocumentError, NewDocument};

pub struct ShiftDocument {
font: shift_ir::Font,
store: shift_store::ShiftStore,
}

impl ShiftDocument {
pub fn create_new(
path: impl AsRef<Path>,
new_document: NewDocument,
) -> Result<Self, DocumentError> {
let mut store = shift_store::ShiftStore::open(path)?;
store.set_font_info(new_document.font_info())?;

Ok(Self {
font: shift_ir::Font::new(),
store,
})
}

pub fn from_parts(font: shift_ir::Font, store: shift_store::ShiftStore) -> Self {
Self { font, store }
}

pub fn font(&self) -> &shift_ir::Font {
&self.font
}

pub fn store(&self) -> &shift_store::ShiftStore {
&self.store
}

pub fn store_mut(&mut self) -> &mut shift_store::ShiftStore {
&mut self.store
}

pub fn font_info(&self) -> Result<Option<shift_store::FontInfo>, DocumentError> {
self.store.get_font_info().map_err(DocumentError::from)
}
}
11 changes: 11 additions & 0 deletions crates/shift-document/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[derive(Debug, thiserror::Error)]
pub enum DocumentError {
#[error("store error")]
Store(#[from] shift_store::StoreError),

#[error("document projection failed: {0}")]
Projection(String),

#[error("document edit failed: {0}")]
Edit(String),
}
7 changes: 7 additions & 0 deletions crates/shift-document/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod document;
mod error;
mod new_document;

pub use document::ShiftDocument;
pub use error::DocumentError;
pub use new_document::NewDocument;
48 changes: 48 additions & 0 deletions crates/shift-document/src/new_document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const DEFAULT_FAMILY_NAME: &str = "Untitled Font";

pub struct NewDocument {
pub family_name: String,
pub units_per_em: i64,
}

impl Default for NewDocument {
fn default() -> Self {
Self {
family_name: DEFAULT_FAMILY_NAME.to_string(),
units_per_em: shift_ir::FontMetrics::default().units_per_em as i64,
}
}
}

impl NewDocument {
pub fn new() -> Self {
Self::default()
}

pub fn with_family_name(family_name: impl Into<String>) -> Self {
Self {
family_name: family_name.into(),
..Self::default()
}
}

pub(crate) fn font_info(&self) -> shift_store::FontInfo {
shift_store::FontInfo {
family_name: Some(self.family_name.clone()),
copyright: None,
trademark: None,
description: None,
sample_text: None,
designer: None,
designer_url: None,
manufacturer: None,
manufacturer_url: None,
license_description: None,
license_info_url: None,
vendor_id: None,
version_major: Some(1),
version_minor: Some(0),
units_per_em: self.units_per_em,
}
}
}
37 changes: 37 additions & 0 deletions crates/shift-document/tests/document_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use shift_document::{NewDocument, ShiftDocument};

#[test]
fn creates_new_document_with_default_font_info() {
let dir = tempfile::tempdir().expect("tempdir should be created");
let path = dir.path().join("font.shift.sqlite");

let document =
ShiftDocument::create_new(&path, NewDocument::new()).expect("document should be created");

let loaded = document
.font_info()
.expect("font info query should succeed")
.expect("font info should exist");

assert_eq!(loaded.family_name.as_deref(), Some("Untitled Font"));
assert_eq!(loaded.version_major, Some(1));
assert_eq!(loaded.version_minor, Some(0));
assert_eq!(loaded.units_per_em, 1000);
}

#[test]
fn creates_new_document_with_custom_family_name() {
let dir = tempfile::tempdir().expect("tempdir should be created");
let path = dir.path().join("font.shift.sqlite");

let document = ShiftDocument::create_new(&path, NewDocument::with_family_name("Shift Sans"))
.expect("document should be created");

let loaded = document
.font_info()
.expect("font info query should succeed")
.expect("font info should exist");

assert_eq!(loaded.family_name.as_deref(), Some("Shift Sans"));
assert_eq!(loaded.units_per_em, 1000);
}
Loading