mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 22:24:06 +00:00
Exports join wrapped Chinese and Japanese lines without a space
Chinese and Japanese text wraps between characters, so the word space put at each joined line break showed up inside the text. No space is added now when both sides of the break are Chinese or Japanese characters, their punctuation or fullwidth forms; Korean, which spaces its words, still gets one. A line ending in a full-width full stop, exclamation or question mark (also inside closing corner brackets) now ends its paragraph like one ending in a full stop. The new test lines fail without the change. The wrapped sentence test now checks the Markdown export too, which goes through the same joining since it was added.
This commit is contained in:
@@ -140,7 +140,25 @@ pub(crate) fn list_item(text: &str) -> bool {
|
||||
/// paragraph spacing gives no other sign that a paragraph ends there, so such a line is left
|
||||
/// apart from the next rather than joined to it wrongly.
|
||||
fn ends_sentence(text: &str) -> bool {
|
||||
text.trim_end().trim_end_matches(['"', '\'', '”', '’', '“', '»', ')', ']']).ends_with(['.', '!', '?', ':', ';', '…'])
|
||||
text.trim_end()
|
||||
.trim_end_matches(['"', '\'', '”', '’', '“', '»', ')', ']', '」', '』'])
|
||||
.ends_with(['.', '!', '?', ':', ';', '…', '。', '!', '?'])
|
||||
}
|
||||
|
||||
/// Whether `c` is of the writing that puts no space between words: Chinese and Japanese
|
||||
/// characters, their punctuation and fullwidth forms. A line that wraps between two of them
|
||||
/// joins without a word space.
|
||||
fn unspaced(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\u{3000}'..='\u{30FF}' // CJK punctuation, Hiragana, Katakana
|
||||
| '\u{31F0}'..='\u{31FF}' // Katakana extension
|
||||
| '\u{3400}'..='\u{4DBF}' // CJK extension A
|
||||
| '\u{4E00}'..='\u{9FFF}' // CJK unified ideographs
|
||||
| '\u{F900}'..='\u{FAFF}' // CJK compatibility ideographs
|
||||
| '\u{FF01}'..='\u{FF9F}' // fullwidth forms, halfwidth Katakana
|
||||
| '\u{20000}'..='\u{3FFFF}' // CJK extension B onwards
|
||||
)
|
||||
}
|
||||
|
||||
/// `paras` (a page's paragraphs, in any order) top to bottom, with each paragraph that wraps onto
|
||||
@@ -169,10 +187,15 @@ pub(crate) fn join_wrapped<'a>(mut paras: Vec<Vec<Run<'a>>>, level: impl Fn(&[Ru
|
||||
match out.last_mut() {
|
||||
Some((prev, 0)) if lvl == 0 && wraps_into(prev, ¶, [left, right]) => {
|
||||
// The line break was a word space, unless the line ends in a hyphen; the hyphen
|
||||
// stays, as it may belong to the word ("well-known").
|
||||
// stays, as it may belong to the word ("well-known"). Chinese and Japanese text
|
||||
// wraps between characters, with no space.
|
||||
let unspaced_break = |end: &str| {
|
||||
end.chars().next_back().is_some_and(unspaced) && para.first().and_then(|r| r.text.chars().next()).is_some_and(unspaced)
|
||||
};
|
||||
if let Some(end) = prev.last_mut()
|
||||
&& !end.text.ends_with(char::is_whitespace)
|
||||
&& !end.text.ends_with('-')
|
||||
&& !unspaced_break(&end.text)
|
||||
{
|
||||
end.text.push(' ');
|
||||
}
|
||||
|
||||
@@ -890,6 +890,9 @@ named in the schedule, <em>Blue Finch Studio</em>, who agree as follows to rent
|
||||
);
|
||||
let r = rtf(std::slice::from_ref(&p));
|
||||
assert_eq!(r.matches("{\\pard").count(), 2, "{r}");
|
||||
let md = markdown(std::slice::from_ref(&p));
|
||||
assert_eq!(md.trim().split("\n\n").count(), 2, "{md}");
|
||||
assert!(md.contains("of 4 Quay Street, and the tenant named in the schedule, "), "{md}");
|
||||
// On a slide the last of the paragraphs set largest is the title; the sentence is one paragraph.
|
||||
let s = part(&pptx(std::slice::from_ref(&p), "Lease"), "ppt/slides/slide1.xml");
|
||||
let slide: Vec<String> =
|
||||
@@ -924,10 +927,21 @@ fn a_paragraph_breaks_where_its_line_does_not_run_on() {
|
||||
let second = |t: &str| line(t, 72.0, 240.0, 686.0);
|
||||
assert_eq!(word_texts(vec![first(full), second(next)]), [format!("{full} {next}")]);
|
||||
assert_eq!(word_texts(vec![first("Изпълнителят поддържа сървърите и мрежата на"), second("възложителя в работно състояние")]).len(), 1);
|
||||
// Chinese and Japanese wrap between characters: their lines join without a space. Korean
|
||||
// spaces its words, so its lines join with one.
|
||||
let zh = ("本合同自双方签字之日起生效,有效期", "为一年,期满前一个月双方协商续签事宜");
|
||||
assert_eq!(word_texts(vec![first(zh.0), second(zh.1)]), [format!("{}{}", zh.0, zh.1)]);
|
||||
assert_eq!(
|
||||
word_texts(vec![first("この契約は双方が署名した日から"), second("一年間有効とする")]),
|
||||
["この契約は双方が署名した日から一年間有効とする"]
|
||||
);
|
||||
assert_eq!(word_texts(vec![first("임차인은 방과 계단을 깨끗하게"), second("유지합니다")]), ["임차인은 방과 계단을 깨끗하게 유지합니다"]);
|
||||
let heading = |b: Block| Block { bold: true, ..b };
|
||||
for (why, blocks) in [
|
||||
("a sentence ends", vec![first(&format!("{full}.")), second(next)]),
|
||||
("a sentence ends in quotes", vec![first("Страните се договориха „за срок от една година.“"), second(next)]),
|
||||
("a Chinese sentence ends", vec![first("本合同自双方签字之日起生效。"), second(zh.1)]),
|
||||
("a Japanese sentence ends in quotes", vec![first("「この契約は一年間有効ですか?」"), second("双方が署名した日から")]),
|
||||
("the line ends short", vec![line(next, 72.0, 240.0, 700.0), line(full, 72.0, 540.0, 686.0)]),
|
||||
("paragraph space", vec![first(full), line(next, 72.0, 240.0, 676.0)]),
|
||||
("an indent", vec![first(full), line(next, 90.0, 258.0, 686.0)]),
|
||||
|
||||
Reference in New Issue
Block a user