2018-01-21 22:35:11 +08:00
|
|
|
|
#![allow(missing_docs)] // FIXME: Document this
|
|
|
|
|
|
2016-03-18 05:41:00 +08:00
|
|
|
|
pub mod fs;
|
2018-01-06 05:03:30 +08:00
|
|
|
|
mod string;
|
2018-01-07 22:10:48 +08:00
|
|
|
|
use errors::Error;
|
2018-03-07 21:02:06 +08:00
|
|
|
|
use regex::Regex;
|
2015-08-07 05:04:19 +08:00
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
use pulldown_cmark::{
|
|
|
|
|
html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES,
|
|
|
|
|
};
|
2018-03-07 21:02:06 +08:00
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
use std::borrow::Cow;
|
2015-09-17 05:35:16 +08:00
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
pub use self::string::{take_lines, RangeArgument};
|
2015-09-17 05:35:16 +08:00
|
|
|
|
|
2018-03-07 21:02:06 +08:00
|
|
|
|
/// Replaces multiple consecutive whitespace characters with a single space character.
|
2019-05-07 02:20:58 +08:00
|
|
|
|
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
2018-03-07 21:02:06 +08:00
|
|
|
|
lazy_static! {
|
|
|
|
|
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
|
|
|
|
|
}
|
|
|
|
|
RE.replace_all(text, " ")
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-09 11:52:32 +08:00
|
|
|
|
/// Convert the given string to a valid HTML element ID.
|
|
|
|
|
/// The only restriction is that the ID must not contain any ASCII whitespace.
|
2018-03-07 21:02:06 +08:00
|
|
|
|
pub fn normalize_id(content: &str) -> String {
|
2018-09-09 11:52:32 +08:00
|
|
|
|
content
|
2018-03-07 21:02:06 +08:00
|
|
|
|
.chars()
|
|
|
|
|
.filter_map(|ch| {
|
|
|
|
|
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
|
|
|
|
Some(ch.to_ascii_lowercase())
|
|
|
|
|
} else if ch.is_whitespace() {
|
|
|
|
|
Some('-')
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2019-05-05 22:57:43 +08:00
|
|
|
|
})
|
|
|
|
|
.collect::<String>()
|
2018-03-07 21:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate an ID for use with anchors which is derived from a "normalised"
|
|
|
|
|
/// string.
|
|
|
|
|
pub fn id_from_content(content: &str) -> String {
|
|
|
|
|
let mut content = content.to_string();
|
|
|
|
|
|
|
|
|
|
// Skip any tags or html-encoded stuff
|
2018-07-24 01:45:01 +08:00
|
|
|
|
const REPL_SUB: &[&str] = &[
|
|
|
|
|
"<em>",
|
|
|
|
|
"</em>",
|
|
|
|
|
"<code>",
|
|
|
|
|
"</code>",
|
|
|
|
|
"<strong>",
|
|
|
|
|
"</strong>",
|
|
|
|
|
"<",
|
|
|
|
|
">",
|
|
|
|
|
"&",
|
|
|
|
|
"'",
|
|
|
|
|
""",
|
|
|
|
|
];
|
2018-03-07 21:02:06 +08:00
|
|
|
|
for sub in REPL_SUB {
|
|
|
|
|
content = content.replace(sub, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove spaces and hashes indicating a header
|
2019-03-23 20:47:10 +08:00
|
|
|
|
let trimmed = content.trim().trim_start_matches('#').trim();
|
2018-03-07 21:02:06 +08:00
|
|
|
|
|
|
|
|
|
normalize_id(trimmed)
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-16 02:19:10 +08:00
|
|
|
|
fn adjust_links<'a>(event: Event<'a>, with_base: &str) -> Event<'a> {
|
2018-07-11 21:33:44 +08:00
|
|
|
|
lazy_static! {
|
|
|
|
|
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
|
2019-01-17 04:44:51 +08:00
|
|
|
|
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match event {
|
|
|
|
|
Event::Start(Tag::Link(dest, title)) => {
|
|
|
|
|
if !HTTP_LINK.is_match(&dest) {
|
2019-01-16 02:19:10 +08:00
|
|
|
|
let dest = if !with_base.is_empty() {
|
|
|
|
|
format!("{}/{}", with_base, dest)
|
|
|
|
|
} else {
|
|
|
|
|
dest.clone().into_owned()
|
|
|
|
|
};
|
|
|
|
|
|
2018-07-11 21:33:44 +08:00
|
|
|
|
if let Some(caps) = MD_LINK.captures(&dest) {
|
|
|
|
|
let mut html_link = [&caps["link"], ".html"].concat();
|
|
|
|
|
|
|
|
|
|
if let Some(anchor) = caps.name("anchor") {
|
|
|
|
|
html_link.push_str(anchor.as_str());
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
return Event::Start(Tag::Link(Cow::from(html_link), title));
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Event::Start(Tag::Link(dest, title))
|
2018-07-24 01:45:01 +08:00
|
|
|
|
}
|
|
|
|
|
_ => event,
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
|
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
2017-06-01 13:28:08 +08:00
|
|
|
|
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
2019-01-16 02:19:10 +08:00
|
|
|
|
render_markdown_with_base(text, curly_quotes, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn render_markdown_with_base(text: &str, curly_quotes: bool, base: &str) -> String {
|
2015-12-30 07:46:55 +08:00
|
|
|
|
let mut s = String::with_capacity(text.len() * 3 / 2);
|
2016-01-03 19:02:39 +08:00
|
|
|
|
|
|
|
|
|
let mut opts = Options::empty();
|
|
|
|
|
opts.insert(OPTION_ENABLE_TABLES);
|
|
|
|
|
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
|
|
|
|
|
2017-02-16 11:01:26 +08:00
|
|
|
|
let p = Parser::new_ext(text, opts);
|
2017-06-01 13:28:08 +08:00
|
|
|
|
let mut converter = EventQuoteConverter::new(curly_quotes);
|
2018-08-03 09:22:49 +08:00
|
|
|
|
let events = p
|
|
|
|
|
.map(clean_codeblock_headers)
|
2019-01-16 02:19:10 +08:00
|
|
|
|
.map(|event| adjust_links(event, base))
|
2018-07-24 01:45:01 +08:00
|
|
|
|
.map(|event| converter.convert(event));
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
html::push_html(&mut s, events);
|
2015-12-30 07:46:55 +08:00
|
|
|
|
s
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
|
|
|
|
|
struct EventQuoteConverter {
|
|
|
|
|
enabled: bool,
|
|
|
|
|
convert_text: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EventQuoteConverter {
|
|
|
|
|
fn new(enabled: bool) -> Self {
|
2017-10-03 19:40:23 +08:00
|
|
|
|
EventQuoteConverter {
|
2018-12-04 07:10:09 +08:00
|
|
|
|
enabled,
|
2017-10-03 19:40:23 +08:00
|
|
|
|
convert_text: true,
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
|
|
|
|
|
if !self.enabled {
|
|
|
|
|
return event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match event {
|
2017-10-03 19:40:23 +08:00
|
|
|
|
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
2017-06-01 13:28:08 +08:00
|
|
|
|
self.convert_text = false;
|
|
|
|
|
event
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
|
|
|
|
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
2017-06-01 13:28:08 +08:00
|
|
|
|
self.convert_text = true;
|
|
|
|
|
event
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
|
|
|
|
Event::Text(ref text) if self.convert_text => {
|
|
|
|
|
Event::Text(Cow::from(convert_quotes_to_curly(text)))
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
_ => event,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-07 04:50:34 +08:00
|
|
|
|
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
|
2017-08-11 17:29:14 +08:00
|
|
|
|
match event {
|
|
|
|
|
Event::Start(Tag::CodeBlock(ref info)) => {
|
2017-10-03 19:40:23 +08:00
|
|
|
|
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
|
|
|
|
Event::Start(Tag::CodeBlock(Cow::from(info)))
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
2017-08-11 17:29:14 +08:00
|
|
|
|
_ => event,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
fn convert_quotes_to_curly(original_text: &str) -> String {
|
|
|
|
|
// We'll consider the start to be "whitespace".
|
|
|
|
|
let mut preceded_by_whitespace = true;
|
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
original_text
|
|
|
|
|
.chars()
|
|
|
|
|
.map(|original_char| {
|
|
|
|
|
let converted_char = match original_char {
|
|
|
|
|
'\'' => {
|
|
|
|
|
if preceded_by_whitespace {
|
|
|
|
|
'‘'
|
|
|
|
|
} else {
|
|
|
|
|
'’'
|
|
|
|
|
}
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
2018-07-24 01:45:01 +08:00
|
|
|
|
'"' => {
|
|
|
|
|
if preceded_by_whitespace {
|
|
|
|
|
'“'
|
|
|
|
|
} else {
|
|
|
|
|
'”'
|
|
|
|
|
}
|
2017-10-03 19:40:23 +08:00
|
|
|
|
}
|
2018-07-24 01:45:01 +08:00
|
|
|
|
_ => original_char,
|
|
|
|
|
};
|
2017-10-03 19:40:23 +08:00
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
preceded_by_whitespace = original_char.is_whitespace();
|
2017-10-03 19:40:23 +08:00
|
|
|
|
|
2018-07-24 01:45:01 +08:00
|
|
|
|
converted_char
|
2019-05-05 22:57:43 +08:00
|
|
|
|
})
|
|
|
|
|
.collect()
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
|
/// Prints a "backtrace" of some `Error`.
|
|
|
|
|
pub fn log_backtrace(e: &Error) {
|
|
|
|
|
error!("Error: {}", e);
|
|
|
|
|
|
|
|
|
|
for cause in e.iter().skip(1) {
|
|
|
|
|
error!("\tCaused By: {}", cause);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
mod render_markdown {
|
|
|
|
|
use super::super::render_markdown;
|
|
|
|
|
|
2018-07-11 21:33:44 +08:00
|
|
|
|
#[test]
|
|
|
|
|
fn preserves_external_links() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example](https://www.rust-lang.org/)", false),
|
|
|
|
|
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
|
|
|
|
|
);
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_adjust_markdown_links() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example](example.md)", false),
|
|
|
|
|
"<p><a href=\"example.html\">example</a></p>\n"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[example_anchor](example.md#anchor)", false),
|
|
|
|
|
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
|
|
|
|
|
);
|
2019-01-17 04:44:51 +08:00
|
|
|
|
|
|
|
|
|
// this anchor contains 'md' inside of it
|
|
|
|
|
assert_eq!(
|
|
|
|
|
render_markdown("[phantom data](foo.html#phantomdata)", false),
|
|
|
|
|
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
|
|
|
|
|
);
|
2018-07-11 21:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_keep_quotes_straight() {
|
|
|
|
|
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
'one'
|
|
|
|
|
```
|
|
|
|
|
'two'
|
|
|
|
|
```
|
|
|
|
|
`'three'` 'four'"#;
|
|
|
|
|
let expected = r#"<p>‘one’</p>
|
|
|
|
|
<pre><code>'two'
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p><code>'three'</code> ‘four’</p>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
2017-08-11 17:29:14 +08:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn whitespace_outside_of_codeblock_header_is_preserved() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
some text with spaces
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
// code inside is unchanged
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
more text with spaces
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let expected = r#"<p>some text with spaces</p>
|
|
|
|
|
<pre><code class="language-rust">fn main() {
|
|
|
|
|
// code inside is unchanged
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>more text with spaces</p>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust,no_run,should_panic,property_3
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
2017-10-03 19:40:23 +08:00
|
|
|
|
let expected =
|
|
|
|
|
r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
|
2017-08-11 17:29:14 +08:00
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust, no_run,,,should_panic , ,property_3
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
2017-10-03 19:40:23 +08:00
|
|
|
|
let expected =
|
|
|
|
|
r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
|
2017-08-11 17:29:14 +08:00
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rust_code_block_without_properties_has_proper_html_class() {
|
|
|
|
|
let input = r#"
|
2017-10-03 19:40:23 +08:00
|
|
|
|
```rust
|
2017-08-11 17:29:14 +08:00
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let expected = r#"<pre><code class="language-rust"></code></pre>
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
|
|
|
|
|
let input = r#"
|
|
|
|
|
```rust
|
|
|
|
|
```
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(render_markdown(input, false), expected);
|
|
|
|
|
assert_eq!(render_markdown(input, true), expected);
|
|
|
|
|
}
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-07 21:02:06 +08:00
|
|
|
|
mod html_munging {
|
|
|
|
|
use super::super::{id_from_content, normalize_id};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_generates_anchors() {
|
2018-09-09 11:47:44 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
id_from_content("## Method-call expressions"),
|
|
|
|
|
"method-call-expressions"
|
|
|
|
|
);
|
2018-12-04 07:10:09 +08:00
|
|
|
|
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
|
|
|
|
|
assert_eq!(id_from_content("## `Code` title"), "code-title");
|
2018-09-09 11:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_generates_anchors_from_non_ascii_initial() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
id_from_content("## `--passes`: add more rustdoc passes"),
|
2018-09-09 11:47:44 +08:00
|
|
|
|
"--passes-add-more-rustdoc-passes"
|
2018-07-24 01:45:01 +08:00
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
2018-09-09 11:47:44 +08:00
|
|
|
|
id_from_content("## 中文標題 CJK title"),
|
|
|
|
|
"中文標題-cjk-title"
|
|
|
|
|
);
|
2018-12-04 07:10:09 +08:00
|
|
|
|
assert_eq!(id_from_content("## Über"), "Über");
|
2018-03-07 21:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_normalizes_ids() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_id("`--passes`: add more rustdoc passes"),
|
2018-09-09 11:47:44 +08:00
|
|
|
|
"--passes-add-more-rustdoc-passes"
|
2018-07-24 01:45:01 +08:00
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
|
|
|
|
|
"method-call--expressions-"
|
|
|
|
|
);
|
2018-09-09 11:47:44 +08:00
|
|
|
|
assert_eq!(normalize_id("_-_12345"), "_-_12345");
|
|
|
|
|
assert_eq!(normalize_id("12345"), "12345");
|
|
|
|
|
assert_eq!(normalize_id("中文"), "中文");
|
|
|
|
|
assert_eq!(normalize_id("にほんご"), "にほんご");
|
|
|
|
|
assert_eq!(normalize_id("한국어"), "한국어");
|
2018-03-07 21:02:06 +08:00
|
|
|
|
assert_eq!(normalize_id(""), "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 13:28:08 +08:00
|
|
|
|
mod convert_quotes_to_curly {
|
|
|
|
|
use super::super::convert_quotes_to_curly;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_converts_single_quotes() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
convert_quotes_to_curly("'one', 'two'"),
|
|
|
|
|
"‘one’, ‘two’"
|
|
|
|
|
);
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_converts_double_quotes() {
|
2018-07-24 01:45:01 +08:00
|
|
|
|
assert_eq!(
|
|
|
|
|
convert_quotes_to_curly(r#""one", "two""#),
|
|
|
|
|
"“one”, “two”"
|
|
|
|
|
);
|
2017-06-01 13:28:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_treats_tab_as_whitespace() {
|
|
|
|
|
assert_eq!(convert_quotes_to_curly("\t'one'"), "\t‘one’");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|