tests for secondary parsers
This commit is contained in:
parent
b2b8546f20
commit
0465fb2e4c
6 changed files with 244 additions and 60 deletions
|
@ -18,6 +18,7 @@ strum = { version = "0.26", features = ["derive"] }
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
tinyaudio = { version = "0.1", optional = true }
|
tinyaudio = { version = "0.1", optional = true }
|
||||||
bng_macros = { path = "bng_macros" }
|
bng_macros = { path = "bng_macros" }
|
||||||
|
const_format = "0.2.33"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["play", "save"]
|
default = ["play", "save"]
|
||||||
|
|
|
@ -2,13 +2,7 @@ extern crate proc_macro;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident};
|
use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Ident};
|
||||||
|
|
||||||
#[proc_macro_derive(ModifierParser)]
|
|
||||||
pub fn modifier_parser(input: TokenStream) -> TokenStream {
|
|
||||||
let ast = parse_macro_input!(input as DeriveInput);
|
|
||||||
impl_modifier_parser(ast)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(SlopeModifierParser)]
|
#[proc_macro_derive(SlopeModifierParser)]
|
||||||
pub fn slope_modifier_parser(input: TokenStream) -> TokenStream {
|
pub fn slope_modifier_parser(input: TokenStream) -> TokenStream {
|
||||||
|
@ -22,44 +16,6 @@ pub fn quick_modifier_parser(input: TokenStream) -> TokenStream {
|
||||||
impl_quick_modifier_parser(ast)
|
impl_quick_modifier_parser(ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn impl_modifier_parser(ast: DeriveInput) -> TokenStream {
|
|
||||||
let name = &ast.ident;
|
|
||||||
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
|
|
||||||
let match_arms = variants.iter().map(|variant| {
|
|
||||||
let variant_name = &variant.ident;
|
|
||||||
let variant_type = if let Fields::Unnamed(ref fields) = variant.fields {
|
|
||||||
&fields.unnamed[0].ty
|
|
||||||
} else {
|
|
||||||
panic!("Expected unnamed fields in enum variants");
|
|
||||||
};
|
|
||||||
let const_name =
|
|
||||||
Ident::new(&variant.ident.to_string().to_uppercase(), Span::call_site());
|
|
||||||
quote! {
|
|
||||||
nom::combinator::map(
|
|
||||||
nom::sequence::preceded(
|
|
||||||
nom::character::complete::char(#name::#const_name),
|
|
||||||
nom::character::complete::#variant_type
|
|
||||||
),
|
|
||||||
#name::#variant_name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl lex::lexer::Parse for #name {
|
|
||||||
fn parse(input: &str) -> nom::IResult<&str, #name> {
|
|
||||||
nom::branch::alt((
|
|
||||||
#(#match_arms),*
|
|
||||||
))(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
panic!("this macro only works on enums")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream {
|
fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream {
|
||||||
let name = &ast.ident;
|
let name = &ast.ident;
|
||||||
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
|
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
|
||||||
|
@ -98,11 +54,11 @@ fn impl_quick_modifier_parser(ast: DeriveInput) -> TokenStream {
|
||||||
nom::branch::alt(
|
nom::branch::alt(
|
||||||
(
|
(
|
||||||
nom::combinator::value(
|
nom::combinator::value(
|
||||||
lex::MORE,
|
lex::UP,
|
||||||
nom::character::complete::char(#name::#const_name.0)
|
nom::character::complete::char(#name::#const_name.0)
|
||||||
),
|
),
|
||||||
nom::combinator::value(
|
nom::combinator::value(
|
||||||
lex::LESS,
|
lex::DOWN,
|
||||||
nom::character::complete::char(#name::#const_name.1)
|
nom::character::complete::char(#name::#const_name.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
use bng_macros::{ModifierParser, QuickModifierParser, SlopeModifierParser};
|
use std::num::{NonZeroU16, NonZeroU8};
|
||||||
|
|
||||||
|
use bng_macros::{QuickModifierParser, SlopeModifierParser};
|
||||||
use fasteval::Instruction;
|
use fasteval::Instruction;
|
||||||
|
|
||||||
mod lex;
|
mod lex;
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
pub(super) enum Atom {
|
pub(super) enum Atom {
|
||||||
Note(char),
|
Note(char),
|
||||||
Rest,
|
Rest,
|
||||||
StartHere,
|
StartHere,
|
||||||
Modifier(Modifier),
|
Modifier(Modifier),
|
||||||
QuickModifier(QuickModifier),
|
QuickModifier(QuickModifier),
|
||||||
Loop(u8, Vec<Atom>),
|
Loop(NonZeroU8, Vec<Atom>),
|
||||||
Tuple(Vec<Atom>),
|
Tuple(Vec<Atom>),
|
||||||
Slope(SlopeModifier, Instruction, Vec<Atom>),
|
Slope(SlopeModifier, Instruction, Vec<Atom>),
|
||||||
Comment,
|
Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
pub(super) enum FlatAtom {
|
pub(super) enum FlatAtom {
|
||||||
Note(char),
|
Note(char),
|
||||||
Rest,
|
Rest,
|
||||||
StartHere,
|
StartHere,
|
||||||
Modifier(Modifier),
|
Modifier(Modifier),
|
||||||
QuickModifier(QuickModifier),
|
QuickModifier(QuickModifier),
|
||||||
LoopStarts(u8),
|
LoopStarts(NonZeroU8),
|
||||||
LoopEnds,
|
LoopEnds,
|
||||||
TupleStarts,
|
TupleStarts,
|
||||||
TupleEnds,
|
TupleEnds,
|
||||||
|
@ -39,12 +43,12 @@ impl Clone for FlatAtom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ModifierParser)]
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
pub(super) enum Modifier {
|
pub(super) enum Modifier {
|
||||||
Volume(u8),
|
Volume(u8),
|
||||||
Octave(u8),
|
Octave(u8),
|
||||||
Length(u8),
|
Length(NonZeroU8),
|
||||||
Tempo(u16),
|
Tempo(NonZeroU16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Modifier {
|
impl Default for Modifier {
|
||||||
|
@ -54,6 +58,7 @@ impl Default for Modifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(QuickModifierParser)]
|
#[derive(QuickModifierParser)]
|
||||||
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
pub(super) enum QuickModifier {
|
pub(super) enum QuickModifier {
|
||||||
Volume(bool),
|
Volume(bool),
|
||||||
Octave(bool),
|
Octave(bool),
|
||||||
|
@ -62,6 +67,7 @@ pub(super) enum QuickModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, SlopeModifierParser)]
|
#[derive(Clone, Copy, SlopeModifierParser)]
|
||||||
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
pub(super) enum SlopeModifier {
|
pub(super) enum SlopeModifier {
|
||||||
Note,
|
Note,
|
||||||
Volume,
|
Volume,
|
||||||
|
|
|
@ -2,10 +2,10 @@ use super::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier};
|
||||||
|
|
||||||
pub(super) mod lexer;
|
pub(super) mod lexer;
|
||||||
|
|
||||||
pub(super) const MORE: bool = true;
|
|
||||||
pub(super) const LESS: bool = false;
|
|
||||||
pub(super) const ON: bool = true;
|
pub(super) const ON: bool = true;
|
||||||
pub(super) const OFF: bool = false;
|
pub(super) const OFF: bool = false;
|
||||||
|
pub(super) const UP: bool = true;
|
||||||
|
pub(super) const DOWN: bool = false;
|
||||||
|
|
||||||
struct WrappingTokens;
|
struct WrappingTokens;
|
||||||
impl WrappingTokens {
|
impl WrappingTokens {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
use std::collections::BTreeMap;
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
num::{NonZeroU16, NonZeroU8},
|
||||||
|
};
|
||||||
|
|
||||||
use clap::builder::TypedValueParser;
|
use clap::builder::TypedValueParser;
|
||||||
use fasteval::{Compiler, Instruction};
|
use fasteval::{Compiler, Instruction};
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::take_till1,
|
bytes::complete::take_till1,
|
||||||
character::complete::{anychar, char, one_of, u8},
|
character::complete::{anychar, char, one_of, u16, u8},
|
||||||
combinator::{map_res, value},
|
combinator::{map_opt, map_res, value},
|
||||||
multi::many0,
|
multi::many0,
|
||||||
sequence::{delimited, pair, preceded, separated_pair},
|
sequence::{delimited, pair, preceded, separated_pair},
|
||||||
Err, IResult, Parser,
|
Err, IResult, Parser,
|
||||||
|
@ -14,18 +17,32 @@ use nom::{
|
||||||
|
|
||||||
use crate::bng::score::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier};
|
use crate::bng::score::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub(crate) trait Parse: Sized {
|
pub(crate) trait Parse: Sized {
|
||||||
fn parse(input: &str) -> IResult<&str, Self>;
|
fn parse(input: &str) -> IResult<&str, Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atom_parser<'a>(notes: &'a str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> {
|
impl Parse for Modifier {
|
||||||
|
fn parse(input: &str) -> IResult<&str, Self> {
|
||||||
|
alt((
|
||||||
|
preceded(char(Modifier::VOLUME), u8).map(Modifier::Volume),
|
||||||
|
preceded(char(Modifier::OCTAVE), u8).map(Modifier::Octave),
|
||||||
|
preceded(char(Modifier::LENGTH), map_opt(u8, NonZeroU8::new)).map(Modifier::Length),
|
||||||
|
preceded(char(Modifier::TEMPO), map_opt(u16, NonZeroU16::new)).map(Modifier::Tempo),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atom_parser(notes: &str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> {
|
||||||
alt((
|
alt((
|
||||||
one_of(notes).map(FlatAtom::Note),
|
one_of(notes).map(FlatAtom::Note),
|
||||||
value(FlatAtom::Rest, char(Atom::REST)),
|
value(FlatAtom::Rest, char(Atom::REST)),
|
||||||
value(FlatAtom::StartHere, char(Atom::START_HERE)),
|
value(FlatAtom::StartHere, char(Atom::START_HERE)),
|
||||||
preceded(char(Atom::MODIFIER), Modifier::parse).map(FlatAtom::Modifier),
|
preceded(char(Atom::MODIFIER), Modifier::parse).map(FlatAtom::Modifier),
|
||||||
QuickModifier::parse.map(FlatAtom::QuickModifier),
|
QuickModifier::parse.map(FlatAtom::QuickModifier),
|
||||||
preceded(char(Atom::LOOP.0), u8).map(FlatAtom::LoopStarts),
|
preceded(char(Atom::LOOP.0), map_opt(u8, NonZeroU8::new)).map(FlatAtom::LoopStarts),
|
||||||
value(FlatAtom::LoopEnds, char(Atom::LOOP.1)),
|
value(FlatAtom::LoopEnds, char(Atom::LOOP.1)),
|
||||||
value(FlatAtom::TupleStarts, char(Atom::TUPLE.0)),
|
value(FlatAtom::TupleStarts, char(Atom::TUPLE.0)),
|
||||||
value(FlatAtom::TupleEnds, char(Atom::TUPLE.1)),
|
value(FlatAtom::TupleEnds, char(Atom::TUPLE.1)),
|
||||||
|
|
204
src/bng/score/lex/lexer/tests.rs
Normal file
204
src/bng/score/lex/lexer/tests.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
mod modifier {
|
||||||
|
use std::num::{NonZeroU16, NonZeroU8};
|
||||||
|
|
||||||
|
use const_format::concatcp;
|
||||||
|
use nom::{
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
Err,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::bng::score::{lex::lexer::Parse, Atom, Modifier};
|
||||||
|
|
||||||
|
pub(super) const SAMPLE_STR: &str = concatcp!(
|
||||||
|
Atom::TUPLE.0,
|
||||||
|
"acc",
|
||||||
|
Atom::TUPLE.1,
|
||||||
|
"ed",
|
||||||
|
Atom::COMMENT.0,
|
||||||
|
"hello"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, Modifier::Volume(2))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::VOLUME, 2u8, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR),
|
||||||
|
ErrorKind::Char
|
||||||
|
))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn octave() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, Modifier::Octave(2))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::OCTAVE, 2u8, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR),
|
||||||
|
ErrorKind::Char
|
||||||
|
))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((
|
||||||
|
SAMPLE_STR,
|
||||||
|
Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) })
|
||||||
|
)),
|
||||||
|
Modifier::parse(concatcp!(Modifier::LENGTH, 2u8, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR),
|
||||||
|
ErrorKind::Char
|
||||||
|
))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR),
|
||||||
|
ErrorKind::Char
|
||||||
|
))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tempo() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((
|
||||||
|
SAMPLE_STR,
|
||||||
|
Modifier::Tempo(unsafe { NonZeroU16::new_unchecked(2) })
|
||||||
|
)),
|
||||||
|
Modifier::parse(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
concatcp!(655353u32, SAMPLE_STR),
|
||||||
|
ErrorKind::Digit
|
||||||
|
))),
|
||||||
|
Modifier::parse(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use modifier::SAMPLE_STR as MODIFIER_SAMPLE_STRING;
|
||||||
|
|
||||||
|
mod quick_modifier {
|
||||||
|
use const_format::concatcp;
|
||||||
|
use nom::{error::Error, Err};
|
||||||
|
|
||||||
|
use super::MODIFIER_SAMPLE_STRING as SAMPLE_STR;
|
||||||
|
use crate::bng::score::{
|
||||||
|
lex::{lexer::Parse, DOWN, OFF, ON, UP},
|
||||||
|
Atom, QuickModifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Volume(UP))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Volume(DOWN))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn octave() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Octave(UP))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Octave(DOWN))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Length(UP))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Length(DOWN))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pizz() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Pizz(ON))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, QuickModifier::Pizz(OFF))),
|
||||||
|
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod slope_modifier {
|
||||||
|
use const_format::concatcp;
|
||||||
|
use nom::{error::Error, Err};
|
||||||
|
|
||||||
|
use crate::bng::score::{lex::lexer::Parse, Atom, SlopeModifier};
|
||||||
|
|
||||||
|
const SAMPLE_STR: &str = concatcp!(" 1-cos((PI*x)/2),acced", Atom::SLOPE.1);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn note() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, SlopeModifier::Note)),
|
||||||
|
SlopeModifier::parse(concatcp!(SlopeModifier::NOTE, SAMPLE_STR))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, SlopeModifier::Volume)),
|
||||||
|
SlopeModifier::parse(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn octave() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, SlopeModifier::Octave)),
|
||||||
|
SlopeModifier::parse(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, SlopeModifier::Length)),
|
||||||
|
SlopeModifier::parse(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tempo() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok((SAMPLE_STR, SlopeModifier::Tempo)),
|
||||||
|
SlopeModifier::parse(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue