M7: preserve ideographic spaces in edited paragraphs (#309)
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
87f635cf65
commit
81a6ca456c
|
|
@ -316,6 +316,26 @@ fn japanese_paragraph_uses_unicode_type3_fallback() {
|
|||
assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_paragraph_keeps_ideographic_spaces() {
|
||||
if without_craft_fonts("japanese_paragraph_keeps_ideographic_spaces") {
|
||||
return;
|
||||
}
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Original paragraph) Tj ET");
|
||||
let replacement = " 見本商会 御中 ";
|
||||
let style = text::BlockStyle { width: Some(400.0), ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(replacement), &style).unwrap();
|
||||
let reopened = reopen(&doc);
|
||||
let lines = text::text_lines(&reopened, 0).unwrap();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].text, replacement);
|
||||
assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement);
|
||||
// Restyling the saved paragraph without new text must also preserve its spacing.
|
||||
let mut doc = reopened;
|
||||
text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { size: Some(14.0), ..style }).unwrap();
|
||||
assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
|
||||
/// Without craft-fonts there is no Japanese face: editing in Japanese is a clear error that
|
||||
/// leaves the page untouched, never a panic; Latin edits work as before.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -800,13 +800,13 @@ fn group_blocks(lines: &[TextLine]) -> Vec<TextBlock> {
|
|||
} else {
|
||||
b.text.push(' ');
|
||||
}
|
||||
b.text.push_str(l.text.trim());
|
||||
b.text.push_str(l.text.trim_matches(paragraph_separator));
|
||||
b.rect = [b.rect[0].min(l.rect[0]), b.rect[1].min(l.rect[1]), b.rect[2].max(l.rect[2]), b.rect[3].max(l.rect[3])];
|
||||
b.lines.push(i);
|
||||
} else {
|
||||
gap = None;
|
||||
blocks.push(TextBlock {
|
||||
text: l.text.trim().to_string(),
|
||||
text: l.text.trim_matches(paragraph_separator).to_string(),
|
||||
rect: l.rect,
|
||||
base_font: l.base_font.clone(),
|
||||
size: l.size,
|
||||
|
|
@ -820,11 +820,17 @@ fn group_blocks(lines: &[TextLine]) -> Vec<TextBlock> {
|
|||
blocks
|
||||
}
|
||||
|
||||
// An ideographic space carries intentional Japanese spacing, including paragraph indentation.
|
||||
// Keep other whitespace normalization unchanged.
|
||||
fn paragraph_separator(c: char) -> bool {
|
||||
c.is_whitespace() && c != '\u{3000}'
|
||||
}
|
||||
|
||||
/// Greedy word wrapping to `width` with `advance` giving a string's width.
|
||||
fn wrap(text: &str, width: f64, advance: impl Fn(&str) -> f64) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut line = String::new();
|
||||
for word in text.split_whitespace() {
|
||||
for word in text.split(paragraph_separator).filter(|word| !word.is_empty()) {
|
||||
let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
|
||||
if !line.is_empty() && advance(&candidate) > width {
|
||||
out.push(std::mem::take(&mut line));
|
||||
|
|
@ -882,7 +888,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
|
|||
let streams = content_streams(doc, &p.dict);
|
||||
let (stream_obj, data) = streams.get(first.stream).cloned().ok_or_else(|| EditError::Invalid("the page's content changed".into()))?;
|
||||
let ops = parse(&data).ops;
|
||||
let text = text.unwrap_or(&b.text).split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let text = text.unwrap_or(&b.text).split(paragraph_separator).filter(|word| !word.is_empty()).collect::<Vec<_>>().join(" ");
|
||||
let o = &first.origin;
|
||||
let (font_name, old_size) = o.state.font.clone().ok_or_else(|| EditError::Invalid("the paragraph has no font".into()))?;
|
||||
let k = o.k.max(1e-6);
|
||||
|
|
@ -1105,3 +1111,17 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
|
|||
})?;
|
||||
Ok(LineEdit { substituted })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn paragraph_wrapping_keeps_ideographic_spaces() {
|
||||
let width = |s: &str| s.chars().count() as f64;
|
||||
assert_eq!(wrap("A B C", 4.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap("A\u{a0}B C", 3.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap("A\u{2028}B C", 3.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap(" \tA B\r\nC ", 3.0, width), ["A B", "C"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1806,8 +1806,8 @@ milestone = "M7"
|
|||
status = "shipped"
|
||||
commands = ["edit.edit_text"]
|
||||
tools = ["text_edit"]
|
||||
evidence = ["crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
|
||||
notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin"
|
||||
evidence = ["crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
|
||||
notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese font-style fallback remains limited (#184)"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.text-move-resize"
|
||||
|
|
|
|||
Loading…
Reference in New Issue