2024-07-26 11:21:00 +02:00
|
|
|
use crate::compiler::compiler::Compiler;
|
|
|
|
use crate::compiler::compiler::Target;
|
|
|
|
use crate::document::document::Document;
|
|
|
|
use crate::document::element::ElemKind;
|
|
|
|
use crate::document::element::Element;
|
|
|
|
use crate::document::element::ReferenceableElement;
|
|
|
|
use crate::lua::kernel::CTX;
|
|
|
|
use crate::parser::parser::Parser;
|
|
|
|
use crate::parser::rule::RegexRule;
|
|
|
|
use crate::parser::source::Source;
|
|
|
|
use crate::parser::source::Token;
|
|
|
|
use ariadne::Fmt;
|
|
|
|
use ariadne::Label;
|
|
|
|
use ariadne::Report;
|
|
|
|
use ariadne::ReportKind;
|
|
|
|
use mlua::Error::BadArgument;
|
|
|
|
use mlua::Function;
|
|
|
|
use mlua::Lua;
|
2024-07-19 11:52:12 +02:00
|
|
|
use regex::Regex;
|
2024-08-03 09:33:21 +02:00
|
|
|
use section_style::SectionLinkPos;
|
|
|
|
use section_style::SectionStyle;
|
2024-07-26 11:21:00 +02:00
|
|
|
use std::ops::Range;
|
|
|
|
use std::rc::Rc;
|
|
|
|
use std::sync::Arc;
|
2024-07-19 11:52:12 +02:00
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Section {
|
2024-07-26 11:21:00 +02:00
|
|
|
pub(self) location: Token,
|
2024-08-03 09:33:21 +02:00
|
|
|
/// Title of the section
|
|
|
|
pub(self) title: String,
|
|
|
|
/// Depth i.e number of '#'
|
|
|
|
pub(self) depth: usize,
|
|
|
|
/// [`section_kind`]
|
|
|
|
pub(self) kind: u8,
|
|
|
|
/// Section reference name
|
|
|
|
pub(self) reference: Option<String>,
|
|
|
|
/// Style of the section
|
|
|
|
pub(self) style: Rc<section_style::SectionStyle>,
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
impl Element for Section {
|
|
|
|
fn location(&self) -> &Token { &self.location }
|
|
|
|
fn kind(&self) -> ElemKind { ElemKind::Block }
|
|
|
|
fn element_name(&self) -> &'static str { "Section" }
|
|
|
|
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
|
|
|
|
match compiler.target() {
|
2024-07-30 16:40:14 +02:00
|
|
|
Target::HTML => {
|
2024-08-03 09:33:21 +02:00
|
|
|
// Section numbering
|
2024-08-03 18:43:54 +02:00
|
|
|
let number = if (self.kind & section_kind::NO_NUMBER) != section_kind::NO_NUMBER {
|
2024-08-02 14:10:17 +02:00
|
|
|
let numbering = compiler.section_counter(self.depth);
|
2024-08-03 18:43:54 +02:00
|
|
|
|
|
|
|
let mut result = String::new();
|
|
|
|
for num in numbering.iter()
|
|
|
|
{
|
|
|
|
result = result + num.to_string().as_str() + ".";
|
|
|
|
}
|
|
|
|
result += " ";
|
|
|
|
|
|
|
|
result
|
2024-08-03 09:33:21 +02:00
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
};
|
|
|
|
|
|
|
|
if self.style.link_pos == SectionLinkPos::None {
|
|
|
|
return Ok(format!(
|
|
|
|
r#"<h{0} id="{1}">{number}{2}</h{0}>"#,
|
|
|
|
self.depth,
|
|
|
|
Compiler::refname(compiler.target(), self.title.as_str()),
|
|
|
|
Compiler::sanitize(compiler.target(), self.title.as_str())
|
|
|
|
));
|
2024-08-02 14:10:17 +02:00
|
|
|
}
|
|
|
|
|
2024-08-03 09:33:21 +02:00
|
|
|
let refname = Compiler::refname(compiler.target(), self.title.as_str());
|
|
|
|
let link = format!(
|
2024-08-03 18:43:54 +02:00
|
|
|
"{}<a class=\"section-link\" href=\"#{refname}\">{}</a>{}",
|
|
|
|
Compiler::sanitize(compiler.target(), self.style.link[0].as_str()),
|
|
|
|
Compiler::sanitize(compiler.target(), self.style.link[1].as_str()),
|
|
|
|
Compiler::sanitize(compiler.target(), self.style.link[2].as_str())
|
2024-08-03 09:33:21 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
if self.style.link_pos == SectionLinkPos::After {
|
|
|
|
Ok(format!(
|
|
|
|
r#"<h{0} id="{1}">{number}{2}{link}</h{0}>"#,
|
|
|
|
self.depth,
|
|
|
|
Compiler::refname(compiler.target(), self.title.as_str()),
|
|
|
|
Compiler::sanitize(compiler.target(), self.title.as_str())
|
|
|
|
))
|
|
|
|
} else
|
|
|
|
// Before
|
|
|
|
{
|
|
|
|
Ok(format!(
|
|
|
|
r#"<h{0} id="{1}">{link}{number}{2}</h{0}>"#,
|
|
|
|
self.depth,
|
|
|
|
Compiler::refname(compiler.target(), self.title.as_str()),
|
|
|
|
Compiler::sanitize(compiler.target(), self.title.as_str())
|
|
|
|
))
|
|
|
|
}
|
2024-08-02 14:10:17 +02:00
|
|
|
}
|
2024-07-26 11:21:00 +02:00
|
|
|
Target::LATEX => Err("Unimplemented compiler".to_string()),
|
|
|
|
}
|
|
|
|
}
|
2024-08-03 09:33:21 +02:00
|
|
|
|
|
|
|
fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) }
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
impl ReferenceableElement for Section {
|
|
|
|
fn reference_name(&self) -> Option<&String> { self.reference.as_ref() }
|
|
|
|
|
|
|
|
fn refcount_key(&self) -> &'static str { "section" }
|
2024-07-26 15:06:09 +02:00
|
|
|
|
|
|
|
fn compile_reference(
|
|
|
|
&self,
|
|
|
|
compiler: &Compiler,
|
2024-07-30 16:40:14 +02:00
|
|
|
_document: &dyn Document,
|
2024-07-26 15:06:09 +02:00
|
|
|
reference: &super::reference::Reference,
|
2024-07-30 16:40:14 +02:00
|
|
|
_refid: usize,
|
2024-07-26 15:06:09 +02:00
|
|
|
) -> Result<String, String> {
|
2024-07-30 16:40:14 +02:00
|
|
|
match compiler.target() {
|
|
|
|
Target::HTML => {
|
|
|
|
let caption = reference.caption().map_or(
|
|
|
|
format!(
|
|
|
|
"({})",
|
|
|
|
Compiler::sanitize(compiler.target(), self.title.as_str())
|
|
|
|
),
|
|
|
|
|cap| cap.clone(),
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(format!(
|
2024-08-03 18:43:54 +02:00
|
|
|
"<a class=\"section-reference\" href=\"#{}\">{caption}</a>",
|
2024-07-30 16:40:14 +02:00
|
|
|
Compiler::refname(compiler.target(), self.title.as_str())
|
|
|
|
))
|
|
|
|
}
|
|
|
|
_ => todo!(""),
|
|
|
|
}
|
2024-07-26 15:06:09 +02:00
|
|
|
}
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct SectionRule {
|
|
|
|
re: [Regex; 1],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SectionRule {
|
|
|
|
pub fn new() -> Self {
|
2024-07-26 11:21:00 +02:00
|
|
|
Self {
|
|
|
|
re: [Regex::new(r"(?:^|\n)(#{1,})(?:\{(.*)\})?((\*|\+){1,})?(.*)").unwrap()],
|
|
|
|
}
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
pub mod section_kind {
|
|
|
|
pub const NONE: u8 = 0x00;
|
|
|
|
pub const NO_TOC: u8 = 0x01;
|
|
|
|
pub const NO_NUMBER: u8 = 0x02;
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl RegexRule for SectionRule {
|
|
|
|
fn name(&self) -> &'static str { "Section" }
|
|
|
|
|
|
|
|
fn regexes(&self) -> &[Regex] { &self.re }
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
fn on_regex_match(
|
|
|
|
&self,
|
|
|
|
_: usize,
|
|
|
|
parser: &dyn Parser,
|
|
|
|
document: &dyn Document,
|
|
|
|
token: Token,
|
|
|
|
matches: regex::Captures,
|
|
|
|
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
|
2024-07-19 11:52:12 +02:00
|
|
|
let mut result = vec![];
|
2024-07-26 11:21:00 +02:00
|
|
|
let section_depth = match matches.get(1) {
|
2024-07-19 11:52:12 +02:00
|
|
|
Some(depth) => {
|
|
|
|
if depth.len() > 6 {
|
|
|
|
result.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), depth.start())
|
|
|
|
.with_message("Invalid section depth")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source(), depth.range()))
|
|
|
|
.with_message(format!("Section is of depth {}, which is greather than {} (maximum depth allowed)",
|
|
|
|
depth.len().fg(parser.colors().info),
|
|
|
|
6.fg(parser.colors().info)))
|
|
|
|
.with_color(parser.colors().error))
|
|
|
|
.finish());
|
2024-07-26 11:21:00 +02:00
|
|
|
return result;
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
depth.len()
|
2024-07-26 11:21:00 +02:00
|
|
|
}
|
2024-07-19 11:52:12 +02:00
|
|
|
_ => panic!("Empty section depth"),
|
|
|
|
};
|
|
|
|
|
|
|
|
// [Optional] Reference name
|
2024-08-02 14:10:17 +02:00
|
|
|
let section_refname =
|
|
|
|
matches.get(2).map_or_else(
|
|
|
|
|| None,
|
|
|
|
|refname| {
|
|
|
|
// Check for duplicate reference
|
|
|
|
if let Some(elem_reference) = document.get_reference(refname.as_str()) {
|
|
|
|
let elem = document.get_from_reference(&elem_reference).unwrap();
|
|
|
|
|
|
|
|
result.push(
|
2024-07-19 11:52:12 +02:00
|
|
|
Report::build(ReportKind::Warning, token.source(), refname.start())
|
|
|
|
.with_message("Duplicate reference name")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source(), refname.range()))
|
|
|
|
.with_message(format!("Reference with name `{}` is already defined in `{}`",
|
|
|
|
refname.as_str().fg(parser.colors().highlight),
|
2024-08-02 13:36:04 +02:00
|
|
|
elem.location().source().name().as_str().fg(parser.colors().highlight)))
|
2024-07-19 11:52:12 +02:00
|
|
|
.with_message(format!("`{}` conflicts with previously defined reference to {}",
|
|
|
|
refname.as_str().fg(parser.colors().highlight),
|
2024-08-02 13:36:04 +02:00
|
|
|
elem.element_name().fg(parser.colors().highlight)))
|
2024-07-19 11:52:12 +02:00
|
|
|
.with_color(parser.colors().warning))
|
2024-07-26 11:21:00 +02:00
|
|
|
.with_label(
|
2024-08-02 13:36:04 +02:00
|
|
|
Label::new((elem.location().source(), elem.location().start()..elem.location().end() ))
|
2024-07-26 11:21:00 +02:00
|
|
|
.with_message(format!("`{}` previously defined here",
|
|
|
|
refname.as_str().fg(parser.colors().highlight)))
|
2024-07-19 11:52:12 +02:00
|
|
|
.with_color(parser.colors().warning))
|
|
|
|
.with_note(format!("Previous reference was overwritten"))
|
|
|
|
.finish());
|
2024-08-02 14:10:17 +02:00
|
|
|
}
|
|
|
|
Some(refname.as_str().to_string())
|
|
|
|
},
|
|
|
|
);
|
2024-07-19 11:52:12 +02:00
|
|
|
|
|
|
|
// Section kind
|
2024-07-26 11:21:00 +02:00
|
|
|
let section_kind = match matches.get(3) {
|
|
|
|
Some(kind) => match kind.as_str() {
|
|
|
|
"*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC,
|
|
|
|
"*" => section_kind::NO_NUMBER,
|
|
|
|
"+" => section_kind::NO_TOC,
|
|
|
|
"" => section_kind::NONE,
|
|
|
|
_ => {
|
|
|
|
result.push(
|
2024-07-19 11:52:12 +02:00
|
|
|
Report::build(ReportKind::Error, token.source(), kind.start())
|
|
|
|
.with_message("Invalid section numbering kind")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source(), kind.range()))
|
|
|
|
.with_message(format!("Section numbering kind must be a combination of `{}` for unnumbered, and `{}` for non-listing; got `{}`",
|
|
|
|
"*".fg(parser.colors().info),
|
|
|
|
"+".fg(parser.colors().info),
|
|
|
|
kind.as_str().fg(parser.colors().highlight)))
|
|
|
|
.with_color(parser.colors().error))
|
|
|
|
.with_help(format!("Leave empty for a numbered listed section"))
|
|
|
|
.finish());
|
2024-07-26 11:21:00 +02:00
|
|
|
return result;
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
2024-07-26 11:21:00 +02:00
|
|
|
},
|
2024-07-19 11:52:12 +02:00
|
|
|
_ => section_kind::NONE,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Spacing + Section name
|
2024-07-26 11:21:00 +02:00
|
|
|
let section_name = match matches.get(5) {
|
2024-07-19 11:52:12 +02:00
|
|
|
Some(name) => {
|
2024-07-26 11:21:00 +02:00
|
|
|
let split = name
|
|
|
|
.as_str()
|
|
|
|
.chars()
|
2024-07-19 11:52:12 +02:00
|
|
|
.position(|c| !c.is_whitespace())
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
|
|
let section_name = &name.as_str()[split..];
|
2024-07-26 11:21:00 +02:00
|
|
|
if section_name.is_empty()
|
|
|
|
// No name
|
2024-07-19 11:52:12 +02:00
|
|
|
{
|
|
|
|
result.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), name.start())
|
2024-07-26 11:21:00 +02:00
|
|
|
.with_message("Missing section name")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source(), name.range()))
|
|
|
|
.with_message("Sections require a name before line end")
|
|
|
|
.with_color(parser.colors().error),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
2024-07-19 11:52:12 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No spacing
|
2024-07-26 11:21:00 +02:00
|
|
|
if split == 0 {
|
2024-07-19 11:52:12 +02:00
|
|
|
result.push(
|
|
|
|
Report::build(ReportKind::Warning, token.source(), name.start())
|
|
|
|
.with_message("Missing section spacing")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source(), name.range()))
|
|
|
|
.with_message("Sections require at least one whitespace before the section's name")
|
|
|
|
.with_color(parser.colors().warning))
|
|
|
|
.with_help(format!("Add a space before `{}`", section_name.fg(parser.colors().highlight)))
|
|
|
|
.finish());
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
section_name.to_string()
|
2024-07-26 11:21:00 +02:00
|
|
|
}
|
|
|
|
_ => panic!("Empty section name"),
|
2024-07-19 11:52:12 +02:00
|
|
|
};
|
|
|
|
|
2024-08-03 09:33:21 +02:00
|
|
|
// Get style
|
|
|
|
let style = parser
|
|
|
|
.current_style(section_style::STYLE_KEY)
|
|
|
|
.downcast_rc::<SectionStyle>()
|
|
|
|
.unwrap();
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
parser.push(
|
|
|
|
document,
|
|
|
|
Box::new(Section {
|
2024-07-21 15:56:56 +02:00
|
|
|
location: token.clone(),
|
2024-07-26 11:21:00 +02:00
|
|
|
title: section_name,
|
|
|
|
depth: section_depth,
|
|
|
|
kind: section_kind,
|
|
|
|
reference: section_refname,
|
2024-08-03 09:33:21 +02:00
|
|
|
style,
|
2024-07-26 11:21:00 +02:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
return result;
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
2024-07-21 15:56:56 +02:00
|
|
|
|
2024-07-30 11:01:22 +02:00
|
|
|
fn lua_bindings<'lua>(&self, lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> {
|
2024-07-21 15:56:56 +02:00
|
|
|
let mut bindings = vec![];
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
bindings.push((
|
|
|
|
"push".to_string(),
|
|
|
|
lua.create_function(
|
|
|
|
|_, (title, depth, kind, reference): (String, usize, String, Option<String>)| {
|
|
|
|
let kind = match kind.as_str() {
|
|
|
|
"*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC,
|
|
|
|
"*" => section_kind::NO_NUMBER,
|
|
|
|
"+" => section_kind::NO_TOC,
|
|
|
|
"" => section_kind::NONE,
|
|
|
|
_ => {
|
|
|
|
return Err(BadArgument {
|
|
|
|
to: Some("push".to_string()),
|
|
|
|
pos: 3,
|
|
|
|
name: Some("kind".to_string()),
|
|
|
|
cause: Arc::new(mlua::Error::external(format!(
|
|
|
|
"Unknown section kind specified"
|
|
|
|
))),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
CTX.with_borrow(|ctx| {
|
|
|
|
ctx.as_ref().map(|ctx| {
|
2024-08-03 09:33:21 +02:00
|
|
|
// Get style
|
|
|
|
let style = ctx
|
|
|
|
.parser
|
|
|
|
.current_style(section_style::STYLE_KEY)
|
|
|
|
.downcast_rc::<SectionStyle>()
|
|
|
|
.unwrap();
|
|
|
|
|
2024-07-26 11:21:00 +02:00
|
|
|
ctx.parser.push(
|
|
|
|
ctx.document,
|
|
|
|
Box::new(Section {
|
|
|
|
location: ctx.location.clone(),
|
|
|
|
title,
|
|
|
|
depth,
|
|
|
|
kind,
|
|
|
|
reference,
|
2024-08-03 09:33:21 +02:00
|
|
|
style,
|
2024-07-26 11:21:00 +02:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
|
2024-07-30 11:01:22 +02:00
|
|
|
Some(bindings)
|
2024-07-21 15:56:56 +02:00
|
|
|
}
|
2024-08-03 09:33:21 +02:00
|
|
|
|
|
|
|
fn register_styles(&self, parser: &dyn Parser) {
|
2024-08-03 11:01:32 +02:00
|
|
|
parser.set_current_style(Rc::new(SectionStyle::default()));
|
2024-08-03 09:33:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mod section_style {
|
|
|
|
use serde::Deserialize;
|
|
|
|
use serde::Serialize;
|
|
|
|
|
|
|
|
use crate::document::style::ElementStyle;
|
|
|
|
use crate::impl_elementstyle;
|
|
|
|
|
|
|
|
pub static STYLE_KEY: &'static str = "style.section";
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
|
|
|
pub enum SectionLinkPos {
|
|
|
|
Before,
|
|
|
|
After,
|
|
|
|
None,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
pub struct SectionStyle {
|
|
|
|
pub link_pos: SectionLinkPos,
|
2024-08-03 18:43:54 +02:00
|
|
|
pub link: [String; 3],
|
2024-08-03 09:33:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for SectionStyle {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
2024-08-03 18:43:54 +02:00
|
|
|
link_pos: SectionLinkPos::Before,
|
|
|
|
link: ["".into(), "🔗".into(), " ".into()],
|
2024-08-03 09:33:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl_elementstyle!(SectionStyle, STYLE_KEY);
|
2024-07-19 11:52:12 +02:00
|
|
|
}
|
2024-08-04 08:32:15 +02:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests
|
|
|
|
{
|
|
|
|
use crate::{parser::{langparser::LangParser, source::SourceFile}, validate_document};
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parser()
|
|
|
|
{
|
|
|
|
let source = Rc::new(SourceFile::with_content(
|
|
|
|
"".to_string(),
|
|
|
|
r#"
|
|
|
|
# 1
|
|
|
|
##+ 2
|
|
|
|
###* 3
|
|
|
|
####+* 4
|
|
|
|
#####*+ 5
|
|
|
|
######{refname} 6
|
|
|
|
"#
|
|
|
|
.to_string(),
|
|
|
|
None,
|
|
|
|
));
|
|
|
|
let parser = LangParser::default();
|
|
|
|
let doc = parser.parse(source, None);
|
|
|
|
|
|
|
|
validate_document!(doc.content().borrow(), 0,
|
|
|
|
Section { depth == 1, title == "1" };
|
|
|
|
Section { depth == 2, title == "2", kind == section_kind::NO_TOC };
|
|
|
|
Section { depth == 3, title == "3", kind == section_kind::NO_NUMBER };
|
|
|
|
Section { depth == 4, title == "4", kind == section_kind::NO_NUMBER | section_kind::NO_TOC };
|
|
|
|
Section { depth == 5, title == "5", kind == section_kind::NO_NUMBER | section_kind::NO_TOC };
|
|
|
|
Section { depth == 6, title == "6", reference == Some("refname".to_string()) };
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|