Skip to content

Commit 6812d57

Browse files
authored
Merge pull request google#100 from google/summary-formatting
Allow formatting in the `SUMMARY.md` file
2 parents 1480904 + 7ea5252 commit 6812d57

File tree

2 files changed

+170
-37
lines changed

2 files changed

+170
-37
lines changed

i18n-helpers/src/bin/mdbook-gettext.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,67 @@ use mdbook::preprocess::{CmdPreprocessor, PreprocessorContext};
3030
use mdbook::BookItem;
3131
use mdbook_i18n_helpers::{extract_events, reconstruct_markdown, translate_events};
3232
use polib::catalog::Catalog;
33+
use polib::message::Message;
3334
use polib::po_file;
35+
use pulldown_cmark::Event;
3436
use semver::{Version, VersionReq};
3537
use std::{io, process};
3638

39+
/// Strip formatting from a Markdown string.
40+
///
41+
/// The string can only contain inline text. Formatting such as
42+
/// emphasis and strong emphasis is removed.
43+
///
44+
/// Modelled after `mdbook::summary::stringify_events`.
45+
fn strip_formatting(text: &str) -> String {
46+
extract_events(text, None)
47+
.iter()
48+
.filter_map(|(_, event)| match event {
49+
Event::Text(text) | Event::Code(text) => Some(text.as_ref()),
50+
Event::SoftBreak => Some(" "),
51+
_ => None,
52+
})
53+
.collect()
54+
}
55+
3756
fn translate(text: &str, catalog: &Catalog) -> String {
3857
let events = extract_events(text, None);
3958
let translated_events = translate_events(&events, catalog);
4059
let (translated, _) = reconstruct_markdown(&translated_events, None);
4160
translated
4261
}
4362

63+
/// Update `catalog` with stripped messages from `SUMMARY.md`.
64+
///
65+
/// While it is permissible to include formatting in the `SUMMARY.md`
66+
/// file, `mdbook` will strip it out when rendering the book. It will
67+
/// also strip formatting when sending the book to preprocessors.
68+
///
69+
/// To be able to find the translations for the `SUMMARY.md` file, we
70+
/// append versions of these messages stripped of formatting.
71+
fn add_stripped_summary_translations(catalog: &mut Catalog) {
72+
let mut stripped_messages = Vec::new();
73+
for msg in catalog.messages() {
74+
// The `SUMMARY.md` filename is fixed, but we cannot assume
75+
// that the file is at `src/SUMMARY.md` since the `src/`
76+
// directory can be configured.
77+
if !msg.source().contains("SUMMARY.md") {
78+
continue;
79+
}
80+
81+
let message = Message::build_singular()
82+
.with_msgid(strip_formatting(msg.msgid()))
83+
.with_msgstr(strip_formatting(msg.msgstr().unwrap()))
84+
.done();
85+
stripped_messages.push(message);
86+
}
87+
88+
for msg in stripped_messages {
89+
catalog.append_or_update(msg);
90+
}
91+
}
92+
93+
/// Translte an entire book.
4494
fn translate_book(ctx: &PreprocessorContext, mut book: Book) -> anyhow::Result<Book> {
4595
// Translation is a no-op when the target language is not set
4696
let language = match &ctx.config.book.language {
@@ -60,9 +110,10 @@ fn translate_book(ctx: &PreprocessorContext, mut book: Book) -> anyhow::Result<B
60110
return Ok(book);
61111
}
62112

63-
let catalog = po_file::parse(&path)
113+
let mut catalog = po_file::parse(&path)
64114
.map_err(|err| anyhow!("{err}"))
65115
.with_context(|| format!("Could not parse {:?} as PO file", path))?;
116+
add_stripped_summary_translations(&mut catalog);
66117
book.for_each_mut(|item| match item {
67118
BookItem::Chapter(ch) => {
68119
ch.content = translate(&ch.content, &catalog);
@@ -114,7 +165,7 @@ fn main() -> anyhow::Result<()> {
114165
#[cfg(test)]
115166
mod tests {
116167
use super::*;
117-
use polib::message::Message;
168+
use polib::message::{Message, MessageMutView};
118169
use polib::metadata::CatalogMetadata;
119170
use pretty_assertions::assert_eq;
120171

@@ -130,6 +181,37 @@ mod tests {
130181
catalog
131182
}
132183

184+
#[test]
185+
fn test_add_stripped_summary_translations() {
186+
// Add two messages which map to the same stripped message.
187+
let mut catalog = create_catalog(&[
188+
("foo `bar`", "FOO `BAR`"),
189+
("**foo** _bar_", "**FOO** _BAR_"),
190+
]);
191+
for (idx, mut msg) in catalog.messages_mut().enumerate() {
192+
// Set the source to SUMMARY.md to ensure
193+
// add_stripped_summary_translations will add a stripped
194+
// version.
195+
*msg.source_mut() = format!("src/SUMMARY.md:{idx}");
196+
}
197+
add_stripped_summary_translations(&mut catalog);
198+
199+
// We now have two messages, one with and one without
200+
// formatting. This lets us handle both the TOC and any
201+
// occurance on the page.
202+
assert_eq!(
203+
catalog
204+
.messages()
205+
.map(|msg| (msg.source(), msg.msgid(), msg.msgstr().unwrap()))
206+
.collect::<Vec<_>>(),
207+
&[
208+
("src/SUMMARY.md:0", "foo `bar`", "FOO `BAR`"),
209+
("src/SUMMARY.md:1", "**foo** _bar_", "**FOO** _BAR_"),
210+
("", "foo bar", "FOO BAR")
211+
]
212+
);
213+
}
214+
133215
#[test]
134216
fn test_translate_single_line() {
135217
let catalog = create_catalog(&[("foo bar", "FOO BAR")]);

i18n-helpers/src/bin/mdbook-xgettext.rs

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,27 @@
2222
use anyhow::{anyhow, Context};
2323
use mdbook::renderer::RenderContext;
2424
use mdbook::BookItem;
25-
use mdbook_i18n_helpers::extract_messages;
25+
use mdbook_i18n_helpers::{extract_events, extract_messages, reconstruct_markdown};
2626
use polib::catalog::Catalog;
2727
use polib::message::Message;
2828
use polib::metadata::CatalogMetadata;
29+
use pulldown_cmark::{Event, Tag};
2930
use std::{fs, io};
3031

32+
/// Strip an optional link from a Markdown string.
33+
fn strip_link(text: &str) -> String {
34+
let events = extract_events(text, None)
35+
.into_iter()
36+
.filter_map(|(_, event)| match event {
37+
Event::Start(Tag::Link(..)) => None,
38+
Event::End(Tag::Link(..)) => None,
39+
_ => Some((0, event)),
40+
})
41+
.collect::<Vec<_>>();
42+
let (without_link, _) = reconstruct_markdown(&events, None);
43+
without_link
44+
}
45+
3146
fn add_message(catalog: &mut Catalog, msgid: &str, source: &str) {
3247
let wrap_options = textwrap::Options::new(76)
3348
.break_words(false)
@@ -59,31 +74,17 @@ fn create_catalog(ctx: &RenderContext) -> anyhow::Result<Catalog> {
5974
let mut catalog = Catalog::new(metadata);
6075

6176
// First, add all chapter names and part titles from SUMMARY.md.
62-
// The book items are in order of the summary, so we can assign
63-
// correct line numbers for duplicate lines by tracking the index
64-
// of our last search.
6577
let summary_path = ctx.config.book.src.join("SUMMARY.md");
6678
let summary = std::fs::read_to_string(ctx.root.join(&summary_path))
6779
.with_context(|| anyhow!("Failed to read {}", summary_path.display()))?;
68-
let mut last_idx = 0;
69-
for item in ctx.book.iter() {
70-
let line = match item {
71-
BookItem::Chapter(chapter) => &chapter.name,
72-
BookItem::PartTitle(title) => title,
73-
BookItem::Separator => continue,
74-
};
75-
76-
let idx = summary[last_idx..].find(line).ok_or_else(|| {
77-
anyhow!(
78-
"Could not find {line:?} in SUMMARY.md after line {} -- \
79-
please remove any formatting from SUMMARY.md",
80-
summary[..last_idx].lines().count()
81-
)
82-
})?;
83-
last_idx += idx;
84-
let lineno = summary[..last_idx].lines().count();
80+
for (lineno, msgid) in extract_messages(&summary) {
8581
let source = format!("{}:{}", summary_path.display(), lineno);
86-
add_message(&mut catalog, line, &source);
82+
// The summary is mostly links like "[Foo *Bar*](foo-bar.md)".
83+
// We strip away the link to get "Foo *Bar*". The formatting
84+
// is stripped away by mdbook when it sends the book to
85+
// mdbook-gettext -- we keep the formatting here in case the
86+
// same text is used for the page title.
87+
add_message(&mut catalog, &strip_link(&msgid), &source);
8788
}
8889

8990
// Next, we add the chapter contents.
@@ -147,6 +148,22 @@ mod tests {
147148
Ok((ctx, tmpdir))
148149
}
149150

151+
#[test]
152+
fn test_strip_link_empty() {
153+
assert_eq!(strip_link(""), "");
154+
}
155+
156+
#[test]
157+
fn test_strip_link_text() {
158+
assert_eq!(strip_link("Summary"), "Summary");
159+
}
160+
161+
#[test]
162+
fn test_strip_link_with_formatting() {
163+
// The formatting is automatically normalized.
164+
assert_eq!(strip_link("[foo *bar* `baz`](foo.md)"), "foo _bar_ `baz`");
165+
}
166+
150167
#[test]
151168
fn test_create_catalog_defaults() -> anyhow::Result<()> {
152169
let (ctx, _tmp) =
@@ -183,29 +200,63 @@ mod tests {
183200

184201
#[test]
185202
fn test_create_catalog_summary_formatting() -> anyhow::Result<()> {
186-
// It is an error to include formatting in the summary file:
187-
// it is stripped by mdbook and we cannot find it later when
188-
// trying to translate the book.
189203
let (ctx, _tmp) = create_render_context(&[
190204
("book.toml", "[book]"),
191-
("src/SUMMARY.md", "- [foo *bar* baz]()"),
205+
(
206+
"src/SUMMARY.md",
207+
"# Summary\n\
208+
\n\
209+
[Prefix Chapter](prefix.md)\n\
210+
\n\
211+
# Part Title\n\
212+
\n\
213+
- [Foo *Bar*](foo.md)\n\
214+
\n\
215+
----------\n\
216+
\n\
217+
- [Baz `Quux`](baz.md)\n\
218+
\n\
219+
[Suffix Chapter](suffix.md)",
220+
),
221+
// Without this, mdbook would automatically create the
222+
// files based on the summary above. This would add
223+
// unnecessary headings below.
224+
("src/prefix.md", ""),
225+
("src/foo.md", ""),
226+
("src/baz.md", ""),
227+
("src/suffix.md", ""),
192228
])?;
193229

194-
assert!(create_catalog(&ctx).is_err());
230+
let catalog = create_catalog(&ctx)?;
231+
assert_eq!(
232+
catalog
233+
.messages()
234+
.map(|msg| msg.msgid())
235+
.collect::<Vec<&str>>(),
236+
&[
237+
"Summary",
238+
"Prefix Chapter",
239+
"Part Title",
240+
"Foo _Bar_",
241+
"Baz `Quux`",
242+
"Suffix Chapter",
243+
]
244+
);
245+
195246
Ok(())
196247
}
197248

198249
#[test]
199250
fn test_create_catalog() -> anyhow::Result<()> {
200251
let (ctx, _tmp) = create_render_context(&[
201252
("book.toml", "[book]"),
202-
("src/SUMMARY.md", "- [The Foo Chapter](foo.md)"),
253+
("src/SUMMARY.md", "- [The *Foo* Chapter](foo.md)"),
203254
(
204255
"src/foo.md",
205256
"# How to Foo\n\
206257
\n\
207-
The first paragraph about Foo.\n\
208-
Still the first paragraph.\n",
258+
First paragraph.\n\
259+
Same paragraph.\n",
209260
),
210261
])?;
211262

@@ -218,12 +269,12 @@ mod tests {
218269
assert_eq!(
219270
catalog
220271
.messages()
221-
.map(|msg| msg.msgid())
222-
.collect::<Vec<&str>>(),
272+
.map(|msg| (msg.source(), msg.msgid()))
273+
.collect::<Vec<_>>(),
223274
&[
224-
"The Foo Chapter",
225-
"How to Foo",
226-
"The first paragraph about Foo. Still the first paragraph.",
275+
("src/SUMMARY.md:1", "The _Foo_ Chapter"),
276+
("src/foo.md:1", "How to Foo"),
277+
("src/foo.md:3", "First paragraph. Same paragraph."),
227278
]
228279
);
229280

0 commit comments

Comments
 (0)