mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-11 21:12:56 +00:00
Merge pull request #6047 from vangel76/fix/5922-doc-header-empty-para
.doc: first body line no longer starts too low under a header
This commit is contained in:
2 files changed
+83
-1
No files matched your search
@@ -348,7 +348,7 @@ fn read_limited(bytes: &[u8], password: Option<&str>, max_paragraphs: usize) ->
|
||||
0 | 1 | 4 => PartKind::Header,
|
||||
_ => PartKind::Footer,
|
||||
};
|
||||
let blocks = story_blocks(&ctx, a, b, &mut pics, max_paragraphs)?;
|
||||
let blocks = header_story_blocks(&ctx, a, b, &mut pics, max_paragraphs)?;
|
||||
if !blocks.is_empty() {
|
||||
let id = doc.add_part(kind, blocks);
|
||||
hf_ids.push((si, k, id));
|
||||
@@ -436,6 +436,36 @@ fn story_blocks(ctx: &WalkCtx<'_>, a: u32, b: u32, pics: &mut media::Pictures, m
|
||||
Ok(table::assemble(paras))
|
||||
}
|
||||
|
||||
/// A header or footer story's blocks without its guard paragraph mark. [MS-DOC] 2.3.3: a
|
||||
/// non-empty header story ends with a paragraph mark that only separates it from the next story
|
||||
/// and is not part of its contents, so a one-line header is stored as `text\r\r`. Laid out, the
|
||||
/// guard would add an empty paragraph to every header and footer and push the body down when
|
||||
/// the header reaches past the top margin (#5922).
|
||||
///
|
||||
/// Only an empty last paragraph after an ordinary one is dropped: a story that is nothing but a
|
||||
/// mark stays a part (an empty header, not one linked to the previous section), and a mark right
|
||||
/// after a table row stays the paragraph that closes the table.
|
||||
fn header_story_blocks(
|
||||
ctx: &WalkCtx<'_>,
|
||||
a: u32,
|
||||
b: u32,
|
||||
pics: &mut media::Pictures,
|
||||
max_paragraphs: usize,
|
||||
) -> Result<wordcraft_doc::Blocks, DocbinError> {
|
||||
let mut paras = walk(ctx, a, b, pics);
|
||||
if ctx.paragraphs.exceeded.get() {
|
||||
return Err(paragraph_limit(max_paragraphs));
|
||||
}
|
||||
let outside_table = |p: &table::ParaOut| p.row.as_ref().is_none_or(|r| !r.in_table && !r.ttp && !r.cell_mark);
|
||||
if let [.., prev, last] = paras.as_slice() {
|
||||
let guard = outside_table(last) && last.para.text.is_empty() && last.para.objects.is_empty() && last.para.section.is_none();
|
||||
if guard && outside_table(prev) {
|
||||
paras.pop();
|
||||
}
|
||||
}
|
||||
Ok(table::assemble(paras))
|
||||
}
|
||||
|
||||
/// A story's CP range `[a, b)`, relative to a subdocument starting at `base` with `len`
|
||||
/// CPs, as absolute CPs clamped to that subdocument; `None` when empty.
|
||||
fn story(base: u32, len: u32, (a, b): (u32, u32)) -> Option<(u32, u32)> {
|
||||
|
||||
@@ -1157,6 +1157,58 @@ fn header_story_becomes_part() {
|
||||
assert!(part.blocks.iter().any(|b| b.as_para().is_some_and(|p| p.text.contains("Header text"))));
|
||||
}
|
||||
|
||||
/// The odd header's blocks for a one-section document whose header story is `story`, with
|
||||
/// `papx_entries` over the whole stream (the header story starts at CP 5, after `body\r`):
|
||||
/// paragraph texts, a table as `<table>`; `None` when the slot has no part.
|
||||
fn odd_header_blocks(story: &'static str, papx_entries: Vec<(u32, u16, Vec<u8>)>) -> Option<Vec<String>> {
|
||||
let spec = StructSpec {
|
||||
text: "body\r",
|
||||
header_stories: vec![story],
|
||||
sections: vec![(5, Vec::new())],
|
||||
chpx_runs: vec![(0, Vec::new())],
|
||||
papx_entries,
|
||||
plf_lst: Vec::new(),
|
||||
plf_lfo: Vec::new(),
|
||||
note_stories: vec![],
|
||||
endnote_stories: vec![],
|
||||
extra_pairs: Vec::new(),
|
||||
data_stream: Vec::new(),
|
||||
};
|
||||
let doc = read(&struct_doc(&spec)).expect("opens");
|
||||
let id = doc.last_section.headers.default?;
|
||||
let part = doc.parts.get(&id).expect("part");
|
||||
Some(part.blocks.iter().map(|b| b.as_para().map_or_else(|| "<table>".to_string(), |p| p.text.clone())).collect())
|
||||
}
|
||||
|
||||
/// #5922: [MS-DOC] 2.3.3 ends every non-empty header story with a guard paragraph mark that
|
||||
/// is not part of its contents. Read as a paragraph, it put an empty line under every header
|
||||
/// and pushed the first body line down when the header reached past the top margin.
|
||||
#[test]
|
||||
fn header_story_guard_mark_is_not_a_paragraph() {
|
||||
let plain = || vec![(0, 0, Vec::new())];
|
||||
// One line typed into the header: content "Header\r" + guard.
|
||||
assert_eq!(odd_header_blocks("Header\r\r", plain()), Some(vec!["Header".to_string()]));
|
||||
// Enter pressed after it: the empty paragraph is content and stays, only the guard goes.
|
||||
assert_eq!(odd_header_blocks("Header\r\r\r", plain()), Some(vec!["Header".to_string(), String::new()]));
|
||||
// An empty header (one empty content paragraph + guard) is still a header of its own.
|
||||
assert_eq!(odd_header_blocks("\r\r", plain()), Some(vec![String::new()]));
|
||||
// A story that is only a mark (no guard) keeps its paragraph and its part.
|
||||
assert_eq!(odd_header_blocks("\r", plain()), Some(vec![String::new()]));
|
||||
// A story without a guard keeps its last paragraph.
|
||||
assert_eq!(odd_header_blocks("Header\r", plain()), Some(vec!["Header".to_string()]));
|
||||
}
|
||||
|
||||
/// #5922: a mark right after a table row in a header story is kept: it is the paragraph that
|
||||
/// closes the table, not a guard to drop.
|
||||
#[test]
|
||||
fn header_story_mark_after_a_table_row_is_kept() {
|
||||
let in_tbl = grpprl(&[(0x2416, &[0x01])]);
|
||||
let ttp: Vec<u8> = [grpprl(&[(0x2416, &[0x01]), (0x2417, &[0x01])]), t_def_table(&[0, 2880])].concat();
|
||||
// Stream: "body\r" (CPs 0-4), then the header story "a\x07" (5-6), row end "\x07" (7), "\r" (8).
|
||||
let papx = vec![(0, 0, Vec::new()), (5, 0, in_tbl), (7, 0, ttp), (8, 0, Vec::new())];
|
||||
assert_eq!(odd_header_blocks("a\u{7}\u{7}\r", papx), Some(vec!["<table>".to_string(), String::new()]));
|
||||
}
|
||||
|
||||
/// A `sprmTDefTable` Prl for `edges` twip boundaries and TC80s of 20 bytes per column,
|
||||
/// built per the spec (cb counts the bytes after it, plus one).
|
||||
fn t_def_table(edges: &[i16]) -> Vec<u8> {
|
||||
|
||||
Reference in new issue
Block a user