diff --git a/crates/engine/src/cmd/place/docx.rs b/crates/engine/src/cmd/place/docx.rs
index 400f4d168..2830fd437 100644
--- a/crates/engine/src/cmd/place/docx.rs
+++ b/crates/engine/src/cmd/place/docx.rs
@@ -19,7 +19,9 @@
//! the color.
//! - Paragraph formatting: alignment (`w:jc`), start and end indents (`w:start` or `w:left`,
//! `w:end` or `w:right`: the right and left indents of a right-to-left paragraph), first-line and
-//! hanging indents, space before and after, and direction (`w:bidi`).
+//! hanging indents, space before and after, and direction (`w:bidi`). A paragraph justified with
+//! a low, medium or high kashida length (`lowKashida`, `mediumKashida`, `highKashida`) is
+//! justified with Short, Medium or Long kashidas; `both` is justified without kashidas.
//! - Styles (§17.7.2): the document defaults, then the paragraph style (else the default paragraph
//! style) and the styles it is based on, then the run's character style (else the default
//! character style) and the styles it is based on, then direct formatting. Bold, italic, caps,
@@ -37,6 +39,7 @@
use std::collections::HashMap;
use roxmltree::Node;
+use vectorcraft_doc::text::Kashida;
use vectorcraft_doc::{CharPosition, CharStyle, Justify, ParaDirection, ParaStyle, ScriptMetrics, TextRun};
use super::text::{self, Story};
@@ -361,8 +364,9 @@ enum Align {
Left,
Center,
Right,
- /// Justified, the last line at the start (the right of a right-to-left paragraph).
- Both,
+ /// Justified, the last line at the start (the right of a right-to-left paragraph), with the
+ /// kashida length of `lowKashida`, `mediumKashida` and `highKashida` (§17.18.44).
+ Both(Kashida),
/// Justified, the last line too.
Distribute,
Start,
@@ -860,8 +864,8 @@ impl Reader {
Align::Left => Justify::Left,
Align::Center => Justify::Center,
Align::Right => Justify::Right,
- Align::Both if rtl => Justify::JustifyRight,
- Align::Both => Justify::JustifyLeft,
+ Align::Both(_) if rtl => Justify::JustifyRight,
+ Align::Both(_) => Justify::JustifyLeft,
Align::Distribute => Justify::JustifyAll,
Align::Start => Justify::Auto,
Align::End if rtl => Justify::Left,
@@ -873,6 +877,7 @@ impl Reader {
space_before: points(p.before),
space_after: points(p.after),
direction: p.rtl.map(|rtl| if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight }),
+ kashida: if let Align::Both(k) = p.align { k } else { Kashida::None },
..ParaStyle::default()
}
})
@@ -898,7 +903,10 @@ fn para_props(ppr: Node) -> ParaProps {
Some("left") => Some(Align::Left),
Some("center") => Some(Align::Center),
Some("right") => Some(Align::Right),
- Some("both" | "lowKashida" | "mediumKashida" | "highKashida") => Some(Align::Both),
+ Some("both") => Some(Align::Both(Kashida::None)),
+ Some("lowKashida") => Some(Align::Both(Kashida::Short)),
+ Some("mediumKashida") => Some(Align::Both(Kashida::Medium)),
+ Some("highKashida") => Some(Align::Both(Kashida::Long)),
Some("distribute" | "thaiDistribute") => Some(Align::Distribute),
Some("start") => Some(Align::Start),
Some("end") => Some(Align::End),
diff --git a/crates/engine/src/tests_textimport.rs b/crates/engine/src/tests_textimport.rs
index 25c3a485b..202e6ffdb 100644
--- a/crates/engine/src/tests_textimport.rs
+++ b/crates/engine/src/tests_textimport.rs
@@ -366,6 +366,47 @@ fn docx_tables_breaks_and_fields_place_as_paragraphs_and_tabs() {
);
}
+#[test]
+fn docx_kashida_justification_places_with_insert_kashidas() {
+ use vectorcraft_doc::Justify;
+ use vectorcraft_doc::text::Kashida;
+ // The document holds right-to-left paragraphs justified with each kashida length and with `both`, a paragraph style
+ // that sets `mediumKashida`, that style under direct formatting, and a left-to-right paragraph.
+ let styles = r#""#;
+ let para = |ppr: &str| format!(r#"{ppr}كتاب جميل"#);
+ let body: String = [
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ ]
+ .map(para)
+ .concat();
+ let mut s = session();
+ let r = s.execute("file.place", &json!({"name": "kashida.docx", "dataBase64": vectorcraft_format::base64_encode(&docx(&body, styles))})).unwrap();
+ let n = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap().clone();
+ let NodeKind::Text(t) = &n.kind else { panic!("not type") };
+ let paras: Vec<_> = t.paragraph_styles().iter().map(|p| (p.justify, p.kashida)).collect();
+ assert_eq!(
+ paras,
+ [
+ (Justify::JustifyRight, Kashida::Short),
+ (Justify::JustifyRight, Kashida::Medium),
+ (Justify::JustifyRight, Kashida::Long),
+ (Justify::JustifyRight, Kashida::None),
+ (Justify::JustifyRight, Kashida::Medium),
+ (Justify::JustifyRight, Kashida::None),
+ (Justify::JustifyLeft, Kashida::Long),
+ (Justify::Center, Kashida::None),
+ ],
+ "low, medium and high kashida lengths are Short, Medium and Long; other alignments have none"
+ );
+}
+
#[test]
fn malformed_docx_fails_cleanly() {
let mut s = session();
diff --git a/docs/mcp.md b/docs/mcp.md
index 8996f8a89..2dae918fd 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -2979,7 +2979,10 @@ left out. `file.place` answers `format: "rtf"`.
A Word document (a ZIP package named `.docx`, or one that holds `word/document.xml`, except a `.txt` file) is placed
the same way and keeps the same formatting, taken from the document defaults, the paragraph style and the character
-style (and the styles each is based on) and direct formatting; theme fonts are read from the theme part. Table cells
+style (and the styles each is based on) and direct formatting; theme fonts are read from the theme part. A paragraph
+justified with a low, medium or high kashida length (`w:jc` `lowKashida`, `mediumKashida` or `highKashida`) is
+justified with `kashida` `"short"`, `"medium"` or `"long"` (see [Insert Kashidas](#insert-kashidas)), and one
+justified with `both` has none. Table cells
are separated by tabs and each row is a paragraph. Hidden and deleted text, field instructions, pictures, text boxes,
headers, footers, notes and list numbers are left out. A package with more than 10,000 files, a part that unpacks to
more than 16 MiB or nests elements more than 256 deep, and a document with more than 4 MiB of text or 100,000
diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md
index 8a5d344a8..925398c16 100644
--- a/docs/target-app-parity.md
+++ b/docs/target-app-parity.md
@@ -811,6 +811,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
- File › Place reads RTF files as area type with their formatting: each run's font, size, bold and italic, underline, strikethrough, caps, superscript or subscript, baseline shift and color, and each paragraph's alignment, indents, space before and after and direction. Table cells are separated by tabs; hidden text, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests_textimport.rs`).
- File › Place reads Word `.docx` documents as area type with the same formatting, taken from the document defaults, the paragraph and character styles (their toggle properties reversing each other) and direct formatting, the theme's fonts included. Table rows are paragraphs with their cells separated by tabs; hidden and deleted text, field instructions, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests_textimport.rs`).
+ - A Word paragraph justified with a low, medium or high kashida length places justified with Insert Kashidas set to Short, Medium or Long (`file.place`, `crates/engine/src/tests_textimport.rs`).
- On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.
- On Windows, a document opened from a mapped network drive finds its links stored with the share's own path (`\\server\share\…`), as the same document opened from the share does; links on other computers stay missing.