diff --git a/fuzz/fuzz_targets/gettext.rs b/fuzz/fuzz_targets/gettext.rs index 86d2c5c1..cd54056c 100644 --- a/fuzz/fuzz_targets/gettext.rs +++ b/fuzz/fuzz_targets/gettext.rs @@ -8,5 +8,5 @@ fuzz_target!(|inputs: (Vec<(&str, &str)>, Vec)| { let (translations, book_items) = inputs; let catalog = create_catalog(translations); let mut book = create_book(book_items); - translate_book(&catalog, &mut book) + let _ = translate_book(&catalog, &mut book); // Err(_) can happen and it's fine. }); diff --git a/fuzz/fuzz_targets/group_events.rs b/fuzz/fuzz_targets/group_events.rs index a19cf283..67a3b450 100644 --- a/fuzz/fuzz_targets/group_events.rs +++ b/fuzz/fuzz_targets/group_events.rs @@ -7,6 +7,7 @@ use pretty_assertions::assert_eq; fuzz_target!(|text: String| { let events = extract_events(&text, None); let flattened_groups = group_events(&events) + .expect("Grouping should succeed") .into_iter() .flat_map(|group| match group { Group::Translate { events, .. } | Group::Skip(events) => events, @@ -16,8 +17,10 @@ fuzz_target!(|text: String| { // Comparison through markdown text to detect missing text. // Events can't be compared directly because `group_events` // may split a event into some events. - let text_from_events = reconstruct_markdown(&events, None); - let text_from_groups = reconstruct_markdown(&flattened_groups, None); + let text_from_events = + reconstruct_markdown(&events, None).expect("Failed to reconstruct Markdown from events"); + let text_from_groups = reconstruct_markdown(&flattened_groups, None) + .expect("Failed to reconstruct Markdown from groups"); assert_eq!(text_from_events, text_from_groups); }); diff --git a/i18n-helpers/src/gettext.rs b/i18n-helpers/src/gettext.rs index 7b071e4b..9a947557 100644 --- a/i18n-helpers/src/gettext.rs +++ b/i18n-helpers/src/gettext.rs @@ -15,11 +15,13 @@ //! This file contains main logic used by the binary `mdbook-gettext`. use super::{extract_events, reconstruct_markdown, translate_events}; +use anyhow::Context; use mdbook::book::Book; use mdbook::BookItem; use polib::catalog::Catalog; use polib::message::Message; use pulldown_cmark::Event; +use pulldown_cmark_to_cmark::Error as CmarkError; /// Strip formatting from a Markdown string. /// @@ -38,11 +40,20 @@ fn strip_formatting(text: &str) -> String { .collect() } -fn translate(text: &str, catalog: &Catalog) -> String { +fn translate(text: &str, catalog: &Catalog) -> anyhow::Result { let events = extract_events(text, None); - let translated_events = translate_events(&events, catalog); - let (translated, _) = reconstruct_markdown(&translated_events, None); - translated + // Translation should always succeed. + let translated_events = translate_events(&events, catalog).expect("Failed to translate events"); + let (translated, _) = reconstruct_markdown(&translated_events, None) + .map_err(|e| match e { + CmarkError::FormatFailed(_) => e.into(), + CmarkError::UnexpectedEvent => anyhow::Error::from(e).context( + "Markdown in translated messages (.po) may not be consistent with the original", + ), + }) + .context("Failed to reconstruct Markdown after translation")?; + // "Failed to reconstruct Markdown after translation" + Ok(translated) } /// Update `catalog` with stripped messages from `SUMMARY.md`. @@ -75,18 +86,29 @@ pub fn add_stripped_summary_translations(catalog: &mut Catalog) { } } -/// Translate an entire book. -pub fn translate_book(catalog: &Catalog, book: &mut Book) { - book.for_each_mut(|item| match item { +fn mutate_item(item: &mut BookItem, catalog: &Catalog) -> anyhow::Result<()> { + match item { BookItem::Chapter(ch) => { - ch.content = translate(&ch.content, catalog); - ch.name = translate(&ch.name, catalog); + ch.content = translate(&ch.content, catalog)?; + ch.name = translate(&ch.name, catalog)?; } BookItem::Separator => {} BookItem::PartTitle(title) => { - *title = translate(title, catalog); + *title = translate(title, catalog)?; } - }); + }; + Ok(()) +} + +/// Translate an entire book. +pub fn translate_book(catalog: &Catalog, book: &mut Book) -> anyhow::Result<()> { + // Unfortunately the `Book` API only allows to mutate *all* the items + // through a clousure, with no early bail-out in case of error. + // Therefore, let's just collect all the results and return the first error + // or `Ok(())`. + let mut results = Vec::>::new(); + book.for_each_mut(|item| results.push(mutate_item(item, catalog))); + results.into_iter().find(|r| r.is_err()).unwrap_or(Ok(())) } #[cfg(test)] @@ -108,6 +130,14 @@ mod tests { catalog } + fn create_book(items: Vec) -> Book { + let mut book = Book::new(); + for item in items { + book.push_item(item); + } + book + } + #[test] fn test_add_stripped_summary_translations() { // Add two messages which map to the same stripped message. @@ -142,28 +172,28 @@ mod tests { #[test] fn test_translate_single_line() { let catalog = create_catalog(&[("foo bar", "FOO BAR")]); - assert_eq!(translate("foo bar", &catalog), "FOO BAR"); + assert_eq!(translate("foo bar", &catalog).unwrap(), "FOO BAR"); } #[test] fn test_translate_single_paragraph() { let catalog = create_catalog(&[("foo bar", "FOO BAR")]); // The output is normalized so the newline disappears. - assert_eq!(translate("foo bar\n", &catalog), "FOO BAR"); + assert_eq!(translate("foo bar\n", &catalog).unwrap(), "FOO BAR"); } #[test] fn test_translate_paragraph_with_leading_newlines() { let catalog = create_catalog(&[("foo bar", "FOO BAR")]); // The output is normalized so the newlines disappear. - assert_eq!(translate("\n\n\nfoo bar\n", &catalog), "FOO BAR"); + assert_eq!(translate("\n\n\nfoo bar\n", &catalog).unwrap(), "FOO BAR"); } #[test] fn test_translate_paragraph_with_trailing_newlines() { let catalog = create_catalog(&[("foo bar", "FOO BAR")]); // The output is normalized so the newlines disappear. - assert_eq!(translate("foo bar\n\n\n", &catalog), "FOO BAR"); + assert_eq!(translate("foo bar\n\n\n", &catalog).unwrap(), "FOO BAR"); } #[test] @@ -177,7 +207,8 @@ mod tests { \n\ last paragraph\n", &catalog - ), + ) + .unwrap(), "first paragraph\n\ \n\ FOO BAR\n\ @@ -203,7 +234,8 @@ mod tests { last\n\ paragraph\n", &catalog - ), + ) + .unwrap(), "FIRST TRANSLATED PARAGRAPH\n\ \n\ LAST TRANSLATED PARAGRAPH" @@ -231,7 +263,7 @@ mod tests { \n\ Text after.\n", &catalog - ), + ).unwrap(), "Text before.\n\ \n\ ```rust,editable\n\ @@ -248,7 +280,7 @@ mod tests { fn test_translate_inline_html() { let catalog = create_catalog(&[("foo bar baz", "FOO BAR BAZ")]); assert_eq!( - translate("foo bar baz", &catalog), + translate("foo bar baz", &catalog).unwrap(), "FOO BAR BAZ" ); } @@ -257,7 +289,7 @@ mod tests { fn test_translate_block_html() { let catalog = create_catalog(&[("foo", "FOO"), ("bar", "BAR")]); assert_eq!( - translate("
\n\nfoo\n\n
\n\nbar\n\n
", &catalog), + translate("
\n\nfoo\n\n
\n\nbar\n\n
", &catalog).unwrap(), "
\n\nFOO\n\n
\n\nBAR\n\n
" ); } @@ -279,7 +311,8 @@ mod tests { | Arrays | `[T; N]` | `[20, 30, 40]` |\n\ | Tuples | `()`, ... | `()`, `('x',)` |", &catalog - ), + ) + .unwrap(), "\ ||TYPES|LITERALS|\n\ |-|-----|--------|\n\ @@ -295,7 +328,7 @@ mod tests { ("More details.", "MORE DETAILS."), ]); assert_eq!( - translate("A footnote[^note].\n\n[^note]: More details.", &catalog), + translate("A footnote[^note].\n\n[^note]: More details.", &catalog).unwrap(), "A FOOTNOTE[^note].\n\n[^note]: MORE DETAILS." ); } @@ -303,7 +336,7 @@ mod tests { #[test] fn test_strikethrough() { let catalog = create_catalog(&[("~~foo~~", "~~FOO~~")]); - assert_eq!(translate("~~foo~~", &catalog), "~~FOO~~"); + assert_eq!(translate("~~foo~~", &catalog).unwrap(), "~~FOO~~"); } #[test] @@ -316,7 +349,8 @@ mod tests { - [ ] Bar\n\ ", &catalog - ), + ) + .unwrap(), "\ - [x] FOO\n\ - [ ] BAR", @@ -327,7 +361,7 @@ mod tests { fn test_heading_attributes() { let catalog = create_catalog(&[("Foo", "FOO"), ("Bar", "BAR")]); assert_eq!( - translate("# Foo { #id .foo }", &catalog), + translate("# Foo { #id .foo }", &catalog).unwrap(), "# FOO { #id .foo }" ); } @@ -343,11 +377,20 @@ mod tests { ````\n\ ", &catalog - ), + ) + .unwrap(), "\ ````d\n\ ```\n\ ````", ); } + + // https://github.com/Byron/pulldown-cmark-to-cmark/issues/91. + #[test] + fn test_translate_book_invalid() { + let catalog = create_catalog(&[("Hello", "Ciao\n---")]); + let mut book = create_book(vec![BookItem::PartTitle(String::from("Hello\n---"))]); + assert!(translate_book(&catalog, &mut book).is_err()); + } } diff --git a/i18n-helpers/src/lib.rs b/i18n-helpers/src/lib.rs index a4244d82..3ab04c10 100644 --- a/i18n-helpers/src/lib.rs +++ b/i18n-helpers/src/lib.rs @@ -27,9 +27,8 @@ use polib::catalog::Catalog; use pulldown_cmark::{ BrokenLinkCallback, CodeBlockKind, DefaultBrokenLinkCallback, Event, LinkType, Tag, TagEnd, }; -use pulldown_cmark_to_cmark::{ - calculate_code_block_token_count, cmark_resume_with_options, Options, State, -}; +use pulldown_cmark_to_cmark::{calculate_code_block_token_count, cmark_resume_with_options}; +use pulldown_cmark_to_cmark::{Error as CmarkError, Options, State}; use std::sync::OnceLock; use syntect::easy::ScopeRangeIterator; use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet}; @@ -219,7 +218,7 @@ impl GroupingContext { /// ], /// ); /// -/// let groups = group_events(&events); +/// let groups = group_events(&events).unwrap(); /// assert_eq!( /// groups, /// vec![ @@ -238,7 +237,7 @@ impl GroupingContext { /// ] /// ); /// ``` -pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { +pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Result>, CmarkError> { #[derive(Debug)] enum State { Translate(usize), @@ -252,8 +251,8 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { idx: usize, events: &'a [(usize, Event<'a>)], mut ctx: GroupingContext, - ) -> (Vec>, GroupingContext) { - match self { + ) -> Result<(Vec>, GroupingContext), CmarkError> { + let groups = match self { State::Translate(start) => { if ctx.skip_next_group { ( @@ -261,7 +260,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { ctx.clear_skip_next_group(), ) } else if is_codeblock_group(&events[start..idx]) { - parse_codeblock(&events[start..idx], ctx) + parse_codeblock(&events[start..idx], ctx)? } else { ( vec![Group::Translate { @@ -273,7 +272,8 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { } } State::Skip(start) => (vec![Group::Skip(events[start..idx].into())], ctx), - } + }; + Ok(groups) } } @@ -289,7 +289,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { Event::Start(Tag::Paragraph | Tag::CodeBlock(..)) => { // A translatable group starts here. let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Translate(idx); @@ -298,7 +298,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { // A translatable group ends after `idx`. let idx = idx + 1; let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Skip(idx); @@ -328,7 +328,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { // translatable group starts here. if let State::Skip(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Translate(idx); @@ -341,7 +341,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { // If in the middle of translation, finish it. if let State::Translate(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); // Restart translation: subtle but should be @@ -357,7 +357,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { // If in the middle of translation, finish it. if let State::Translate(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); // Restart translation: subtle but should be @@ -374,7 +374,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { // Otherwise, treat as a skipping group if this is a block level Html tag if let State::Translate(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Skip(idx); @@ -386,7 +386,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { { if let State::Skip(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Translate(idx); @@ -404,7 +404,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { _ => { if let State::Translate(_) = state { let mut next_groups; - (next_groups, ctx) = state.into_groups(idx, events, ctx); + (next_groups, ctx) = state.into_groups(idx, events, ctx)?; groups.append(&mut next_groups); state = State::Skip(idx); @@ -421,7 +421,7 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec> { State::Skip(start) => groups.push(Group::Skip(events[start..].into())), } - groups + Ok(groups) } /// Returns true if the events appear to be a codeblock. @@ -450,10 +450,10 @@ fn is_translate_scope(x: Scope) -> bool { fn heuristic_codeblock<'a>( events: &'a [(usize, Event<'_>)], mut ctx: GroupingContext, -) -> (Vec>, GroupingContext) { +) -> Result<(Vec>, GroupingContext), CmarkError> { let is_translate = match events { [(_, Event::Start(Tag::CodeBlock(_))), .., (_, Event::End(TagEnd::CodeBlock))] => { - let (codeblock_text, _) = reconstruct_markdown(events, None); + let (codeblock_text, _) = reconstruct_markdown(events, None)?; // Heuristic to check whether the codeblock nether has a // literal string nor a line comment. We may actually // want to use a lexer here to make this more robust. @@ -462,7 +462,7 @@ fn heuristic_codeblock<'a>( _ => true, }; - if is_translate { + let (groups, ctx) = if is_translate { ( vec![Group::Translate { events: events.into(), @@ -472,14 +472,15 @@ fn heuristic_codeblock<'a>( ) } else { (vec![Group::Skip(events.into())], ctx) - } + }; + Ok((groups, ctx)) } /// Creates groups by parsing codeblock. fn parse_codeblock<'a>( events: &'a [(usize, Event<'_>)], mut ctx: GroupingContext, -) -> (Vec>, GroupingContext) { +) -> Result<(Vec>, GroupingContext), CmarkError> { // Language detection from language identifier of codeblock. static SYNTAX_SET: OnceLock = OnceLock::new(); let ss = SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines); @@ -586,7 +587,7 @@ fn parse_codeblock<'a>( } } } - (ret, ctx) + Ok((ret, ctx)) } /// Extract trailing events which have whitespace only. @@ -615,7 +616,7 @@ fn extract_trailing_whitespaces<'a>(buf: &mut Vec<(usize, Event<'a>)>) -> Vec<(u /// use pulldown_cmark::{Event, Tag}; /// /// let group = extract_events("Hello *world!*", None); -/// let (reconstructed, _) = reconstruct_markdown(&group, None); +/// let (reconstructed, _) = reconstruct_markdown(&group, None).unwrap(); /// assert_eq!(reconstructed, "Hello _world!_"); /// ``` /// @@ -626,7 +627,7 @@ fn extract_trailing_whitespaces<'a>(buf: &mut Vec<(usize, Event<'a>)>) -> Vec<(u pub fn reconstruct_markdown<'a>( group: &[(usize, Event<'a>)], state: Option>, -) -> (String, State<'a>) { +) -> Result<(String, State<'a>), CmarkError> { let events = group.iter().map(|(_, event)| event); let code_block_token_count = calculate_code_block_token_count(events.clone()).unwrap_or(3); let mut markdown = String::new(); @@ -644,31 +645,24 @@ pub fn reconstruct_markdown<'a>( String::new(), state.clone(), options.clone(), - ) - .unwrap(); + )?; // Block quotes and lists add padding to the state, which is // reflected in the rendered Markdown. We want to capture the // Markdown without the padding to remove the effect of these // structural elements. Similarly, we don't want extra newlines at // the start. - let simplified_state = { - // Because State is marked as non_exhaustive, we can't do - // more intuitive mapping/constructing a new instance. Instead, - // doing a clone and in-place mutation. - let mut cloned_state = state.clone(); - if let Some(ref mut state) = cloned_state { - state.newlines_before_start = 0; - state.padding = Vec::new(); - } - cloned_state - }; - cmark_resume_with_options(events, &mut markdown, simplified_state, options).unwrap(); + let simplified_state = state.map(|mut state| { + state.newlines_before_start = 0; + state.padding.clear(); + state + }); + cmark_resume_with_options(events, &mut markdown, simplified_state, options)?; // Even with `newlines_before_start` set to zero, we get a leading // `\n` for code blocks (since they must start on a new line). We // can safely trim this here since we know that we always // reconstruct Markdown for a self-contained group of events. - (String::from(markdown.trim_start_matches('\n')), new_state) + Ok((String::from(markdown.trim_start_matches('\n')), new_state)) } #[derive(Debug, PartialEq)] @@ -696,14 +690,14 @@ impl From<&str> for ExtractedMessage { /// use mdbook_i18n_helpers::extract_messages; /// /// assert_eq!( -/// extract_messages("# A heading"), +/// extract_messages("# A heading").unwrap(), /// vec![(1, "A heading".into())], /// ); /// assert_eq!( /// extract_messages( /// "1. First item\n\ /// 2. Second item\n" -/// ), +/// ).unwrap(), /// vec![ /// (1, "First item".into()), /// (2, "Second item".into()), @@ -723,7 +717,7 @@ impl From<&str> for ExtractedMessage { /// >\n\ /// > This is the second\n\ /// > paragraph.\n" -/// ); +/// ).unwrap(); /// assert_eq!( /// messages, /// vec![ @@ -732,16 +726,16 @@ impl From<&str> for ExtractedMessage { /// ], /// ); /// ``` -pub fn extract_messages(document: &str) -> Vec<(usize, ExtractedMessage)> { +pub fn extract_messages(document: &str) -> Result, CmarkError> { let events = extract_events(document, None); let mut messages = Vec::new(); let mut state = None; - for group in group_events(&events) { + for group in group_events(&events)? { match group { Group::Translate { events, comment } => { if let Some((lineno, _)) = events.first() { - let (text, new_state) = reconstruct_markdown(&events, state); + let (text, new_state) = reconstruct_markdown(&events, state)?; // Skip empty messages since they are special: // they contains the PO file metadata. if !text.trim().is_empty() { @@ -757,13 +751,13 @@ pub fn extract_messages(document: &str) -> Vec<(usize, ExtractedMessage)> { } } Group::Skip(events) => { - let (_, new_state) = reconstruct_markdown(&events, state); + let (_, new_state) = reconstruct_markdown(&events, state)?; state = Some(new_state); } } } - messages + Ok(messages) } /// Trim `new_events` if they're wrapped in an unwanted paragraph. @@ -777,7 +771,7 @@ pub fn extract_messages(document: &str) -> Vec<(usize, ExtractedMessage)> { /// use mdbook_i18n_helpers::{extract_events, reconstruct_markdown, trim_paragraph}; /// /// let old_events = vec![(1, Event::Text("A line of text".into()))]; -/// let (markdown, _) = reconstruct_markdown(&old_events, None); +/// let (markdown, _) = reconstruct_markdown(&old_events, None).unwrap(); /// let new_events = extract_events(&markdown, None); /// // The stand-alone text has been wrapped in an extra paragraph: /// assert_eq!( @@ -813,15 +807,15 @@ pub fn trim_paragraph<'a, 'event>( pub fn translate_events<'a>( events: &'a [(usize, Event<'a>)], catalog: &'a Catalog, -) -> Vec<(usize, Event<'a>)> { +) -> Result)>, CmarkError> { let mut translated_events = Vec::new(); let mut state = None; - for group in group_events(events) { + for group in group_events(events)? { match group { Group::Translate { events, .. } => { // Reconstruct the message. - let (msgid, new_state) = reconstruct_markdown(&events, state.clone()); + let (msgid, new_state) = reconstruct_markdown(&events, state.clone())?; let translated = catalog .find_message(None, &msgid, None) .filter(|msg| !msg.flags().is_fuzzy() && msg.is_translated()) @@ -844,13 +838,13 @@ pub fn translate_events<'a>( // Copy the events unchanged to the output. translated_events.extend_from_slice(&events); // Advance the state. - let (_, new_state) = reconstruct_markdown(&events, state); + let (_, new_state) = reconstruct_markdown(&events, state)?; state = Some(new_state); } } } - translated_events + Ok(translated_events) } #[cfg(test)] @@ -867,6 +861,7 @@ mod tests { fn assert_extract_messages(document: &str, expected: &[(usize, &str)]) { assert_eq!( extract_messages(document) + .unwrap() .iter() .map(|(lineno, msg)| (*lineno, &msg.message[..])) .collect::>(), @@ -942,7 +937,7 @@ mod tests { #[test] fn extract_events_code_block() { let (_, state) = - reconstruct_markdown(&[(1, Start(CodeBlock(CodeBlockKind::Indented)))], None); + reconstruct_markdown(&[(1, Start(CodeBlock(CodeBlockKind::Indented)))], None).unwrap(); assert_eq!( extract_events("foo\nbar\nbaz", Some(state)), vec![ @@ -1695,7 +1690,8 @@ def g(x): Hello world! " - ), + ) + .unwrap(), vec![( 3, ExtractedMessage { @@ -1715,7 +1711,8 @@ Hello world! Greetings! " - ), + ) + .unwrap(), vec![( 4, ExtractedMessage { @@ -1742,7 +1739,8 @@ after after-no-comment " - ), + ) + .unwrap(), vec![ ( 2, @@ -1786,7 +1784,8 @@ after-no-comment print("Hello world") ``` "# - ), + ) + .unwrap(), vec![( 4, ExtractedMessage { diff --git a/i18n-helpers/src/normalize.rs b/i18n-helpers/src/normalize.rs index 31a28f64..b90d7ad9 100644 --- a/i18n-helpers/src/normalize.rs +++ b/i18n-helpers/src/normalize.rs @@ -7,6 +7,7 @@ use std::fs::File; use std::io::Read; use crate::{extract_messages, new_cmark_parser, wrap_sources}; +use anyhow::Context; use chrono::Duration; use polib::catalog::Catalog; use polib::message::{Message, MessageFlags, MessageMutView, MessageView}; @@ -18,11 +19,12 @@ fn parse_source(source: &str) -> Option<(&str, usize)> { } /// Use only the potion of extract_messages that the normalizer cares about. -fn extract_document_messages(doc: &str) -> Vec<(usize, String)> { - extract_messages(doc) +fn extract_document_messages(doc: &str) -> anyhow::Result> { + Ok(extract_messages(doc) + .context("Failed to extract messages")? .into_iter() .map(|(idx, extracted)| (idx, extracted.message)) - .collect() + .collect()) } fn compute_source(source: &str, delta: usize) -> String { @@ -120,7 +122,7 @@ impl<'a> SourceMap<'a> { // link should be defined. let document = field.project(message.msgid(), message.msgstr()?); if !has_broken_link(document) { - return Ok(extract_document_messages(document)); + return extract_document_messages(document); } // If `parse_source` fails, then `message` has more than one @@ -128,7 +130,7 @@ impl<'a> SourceMap<'a> { // case since it is unclear which link definition to use. let path = match parse_source(message.source()) { Some((path, _)) => path, - None => return Ok(extract_document_messages(document)), + None => return extract_document_messages(document), }; // First, we try constructing a document using other messages @@ -159,7 +161,7 @@ impl<'a> SourceMap<'a> { let _ = file.read_to_string(&mut full_document); } - let mut messages = extract_document_messages(&full_document); + let mut messages = extract_document_messages(&full_document)?; // Truncate away the messages from `full_document` which start // after `document`. let line_count = document.lines().count(); diff --git a/i18n-helpers/src/preprocessors/gettext.rs b/i18n-helpers/src/preprocessors/gettext.rs index 10e2e510..42d5fd02 100644 --- a/i18n-helpers/src/preprocessors/gettext.rs +++ b/i18n-helpers/src/preprocessors/gettext.rs @@ -80,7 +80,7 @@ impl Preprocessor for Gettext { if should_translate(ctx) { let mut catalog = load_catalog(ctx)?; add_stripped_summary_translations(&mut catalog); - translate_book(&catalog, &mut book); + translate_book(&catalog, &mut book)?; } Ok(book) } diff --git a/i18n-helpers/src/xgettext.rs b/i18n-helpers/src/xgettext.rs index 0b7640e1..d2213c5f 100644 --- a/i18n-helpers/src/xgettext.rs +++ b/i18n-helpers/src/xgettext.rs @@ -40,7 +40,8 @@ fn strip_link(text: &str) -> String { _ => Some((0, event)), }) .collect::>(); - let (without_link, _) = reconstruct_markdown(&events, None); + let (without_link, _) = reconstruct_markdown(&events, None) + .unwrap_or_else(|_| panic!("Couldn't strip link \"{}\"", text)); without_link } @@ -143,7 +144,7 @@ where let summary_path = ctx.config.book.src.join("SUMMARY.md"); let summary = summary_reader(ctx.root.join(&summary_path)) .with_context(|| anyhow!("Failed to read {}", summary_path.display()))?; - for (lineno, extracted_msg) in extract_messages(&summary) { + for (lineno, extracted_msg) in extract_messages(&summary)? { let msgid = extracted_msg.message; let source = build_source(&summary_path, lineno, granularity); // The summary is mostly links like "[Foo *Bar*](foo-bar.md)". @@ -211,7 +212,10 @@ where .entry(destination) .or_insert(Catalog::new(generate_catalog_metadata(ctx))); let path = ctx.config.book.src.join(&source); - for (lineno, extracted) in extract_messages(&content) { + for (lineno, extracted) in extract_messages(&content).context(anyhow!( + "Failed to extract messages in chapter {}", + chapter.name + ))? { let msgid = extracted.message; let source = build_source(&path, lineno, granularity); add_message(catalog, &msgid, &source, &extracted.comment);