mirror of
https://codeberg.org/icewind/vdf-reader.git
synced 2026-06-03 18:14:07 +02:00
make bare sequences worth with Entry
This commit is contained in:
parent
fe7bc149d6
commit
d29d06166c
10 changed files with 145 additions and 24 deletions
|
|
@ -10,6 +10,7 @@ pub use statement::Statement;
|
||||||
use std::any::type_name;
|
use std::any::type_name;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
|
use std::mem::swap;
|
||||||
use std::slice;
|
use std::slice;
|
||||||
pub use table::Table;
|
pub use table::Table;
|
||||||
pub use value::Value;
|
pub use value::Value;
|
||||||
|
|
@ -116,9 +117,7 @@ impl Entry {
|
||||||
pub fn as_str(&self) -> Option<&str> {
|
pub fn as_str(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Entry::Value(value) => Some(value),
|
Entry::Value(value) => Some(value),
|
||||||
|
|
||||||
Entry::Statement(value) => Some(value),
|
Entry::Statement(value) => Some(value),
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +135,32 @@ impl Entry {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_array(self) -> Result<Self, ParseEntryError> {
|
||||||
|
match self {
|
||||||
|
Entry::Array(array) => Ok(Entry::Array(array)),
|
||||||
|
Entry::Value(value) => {
|
||||||
|
let mut array = Array::default();
|
||||||
|
array.push(value.into());
|
||||||
|
Ok(Entry::Array(array))
|
||||||
|
}
|
||||||
|
entry => Err(ParseEntryError::new("array", entry)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to take the entry as a slice.
|
||||||
|
pub fn push(&mut self, value: Entry) -> Result<(), ParseEntryError> {
|
||||||
|
let mut tmp = Entry::Value(Value::default());
|
||||||
|
|
||||||
|
swap(self, &mut tmp);
|
||||||
|
*self = tmp.into_array()?;
|
||||||
|
if let Entry::Array(array) = self {
|
||||||
|
array.push(value);
|
||||||
|
} else {
|
||||||
|
panic!("into_array ensured this is an array")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsable types.
|
/// Parsable types.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{Array, Entry};
|
use super::{Array, Entry};
|
||||||
use crate::entry::{string_is_array, Statement, Value};
|
use crate::entry::{string_is_array, Statement, Value};
|
||||||
use crate::error::UnknownError;
|
use crate::error::UnknownError;
|
||||||
use crate::event::{EntryEvent, GroupStartEvent};
|
use crate::event::{EntryEvent, GroupStartEvent, ValueContinuationEvent};
|
||||||
use crate::{Event, Item, Reader, Result, VdfError};
|
use crate::{Event, Item, Reader, Result, VdfError};
|
||||||
use serde::de::{DeserializeSeed, MapAccess};
|
use serde::de::{DeserializeSeed, MapAccess};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
@ -63,15 +63,17 @@ impl Table {
|
||||||
/// Load a table from the given `Reader`.
|
/// Load a table from the given `Reader`.
|
||||||
pub fn load(reader: &mut Reader) -> Result<Table> {
|
pub fn load(reader: &mut Reader) -> Result<Table> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
let mut last_key = None;
|
||||||
|
|
||||||
while let Some(event) = reader.event() {
|
while let Some(event) = reader.event() {
|
||||||
match event? {
|
last_key = match event? {
|
||||||
Event::Entry(EntryEvent {
|
Event::Entry(EntryEvent {
|
||||||
key: Item::Item { content: key, .. },
|
key: Item::Item { content: key, .. },
|
||||||
value,
|
value,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let str = value.as_str();
|
let str = value.as_str();
|
||||||
|
let key_clone = key.clone();
|
||||||
if string_is_array(str) {
|
if string_is_array(str) {
|
||||||
insert(
|
insert(
|
||||||
&mut map,
|
&mut map,
|
||||||
|
|
@ -81,16 +83,31 @@ impl Table {
|
||||||
} else {
|
} else {
|
||||||
insert(&mut map, key, Value::from(value.into_content()))
|
insert(&mut map, key, Value::from(value.into_content()))
|
||||||
}
|
}
|
||||||
|
Some(key_clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Entry(EntryEvent {
|
Event::Entry(EntryEvent {
|
||||||
key: Item::Statement { content: key, .. },
|
key: Item::Statement { content: key, .. },
|
||||||
value,
|
value,
|
||||||
..
|
..
|
||||||
}) => insert(&mut map, key, Statement::from(value.into_content())),
|
}) => {
|
||||||
|
let key_clone = key.clone();
|
||||||
|
insert(&mut map, key, Statement::from(value.into_content()));
|
||||||
|
Some(key_clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::ValueContinuation(ValueContinuationEvent { value, .. }) => {
|
||||||
|
if let Some(key) = last_key.as_ref() {
|
||||||
|
if let Some(last_value) = map.get_mut(key.as_ref()) {
|
||||||
|
last_value.push(Value::from(value.into_content()).into())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_key
|
||||||
|
}
|
||||||
|
|
||||||
Event::GroupStart(GroupStartEvent { name, .. }) => {
|
Event::GroupStart(GroupStartEvent { name, .. }) => {
|
||||||
insert(&mut map, name, Table::load(reader)?)
|
insert(&mut map, name, Table::load(reader)?);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::GroupEnd(_) => break,
|
Event::GroupEnd(_) => break,
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ impl<'de> Deserialize<'de> for Value {
|
||||||
write!(formatter, "any string like value")
|
write!(formatter, "any string like value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
Ok(if v { "1".into() } else { "0".into() })
|
||||||
|
}
|
||||||
|
|
||||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: Error,
|
E: Error,
|
||||||
|
|
@ -116,13 +123,6 @@ impl<'de> Deserialize<'de> for Value {
|
||||||
{
|
{
|
||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(if v { "1".into() } else { "0".into() })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializer.deserialize_str(ValueVisitor).map(Value)
|
deserializer.deserialize_str(ValueVisitor).map(Value)
|
||||||
|
|
|
||||||
10
src/error.rs
10
src/error.rs
|
|
@ -21,7 +21,7 @@ pub enum VdfError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(transparent)]
|
#[diagnostic(transparent)]
|
||||||
/// Wrong event to for conversion
|
/// Wrong event to for conversion
|
||||||
WrongEntryType(Box<WrongEventTypeError>),
|
WrongEventType(Box<WrongEventTypeError>),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(transparent)]
|
#[diagnostic(transparent)]
|
||||||
/// Failed to parse entry into type
|
/// Failed to parse entry into type
|
||||||
|
|
@ -49,7 +49,7 @@ pub enum VdfError {
|
||||||
|
|
||||||
impl From<WrongEventTypeError> for VdfError {
|
impl From<WrongEventTypeError> for VdfError {
|
||||||
fn from(value: WrongEventTypeError) -> Self {
|
fn from(value: WrongEventTypeError) -> Self {
|
||||||
Self::WrongEntryType(value.into())
|
Self::WrongEventType(value.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl VdfError {
|
||||||
VdfError::Other(e) => e.src.as_str(),
|
VdfError::Other(e) => e.src.as_str(),
|
||||||
VdfError::UnexpectedToken(e) => e.src.as_str(),
|
VdfError::UnexpectedToken(e) => e.src.as_str(),
|
||||||
VdfError::NoValidToken(e) => e.src.as_str(),
|
VdfError::NoValidToken(e) => e.src.as_str(),
|
||||||
VdfError::WrongEntryType(e) => e.src.as_str(),
|
VdfError::WrongEventType(e) => e.src.as_str(),
|
||||||
VdfError::SerdeParse(e) => e.src.as_str(),
|
VdfError::SerdeParse(e) => e.src.as_str(),
|
||||||
VdfError::UnknownVariant(e) => e.src.as_str(),
|
VdfError::UnknownVariant(e) => e.src.as_str(),
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -73,7 +73,7 @@ impl VdfError {
|
||||||
VdfError::Other(e) => e.err_span,
|
VdfError::Other(e) => e.err_span,
|
||||||
VdfError::UnexpectedToken(e) => e.err_span,
|
VdfError::UnexpectedToken(e) => e.err_span,
|
||||||
VdfError::NoValidToken(e) => e.err_span,
|
VdfError::NoValidToken(e) => e.err_span,
|
||||||
VdfError::WrongEntryType(e) => e.err_span,
|
VdfError::WrongEventType(e) => e.err_span,
|
||||||
VdfError::SerdeParse(e) => e.err_span,
|
VdfError::SerdeParse(e) => e.err_span,
|
||||||
VdfError::UnknownVariant(e) => e.err_span,
|
VdfError::UnknownVariant(e) => e.err_span,
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -118,7 +118,7 @@ impl VdfError {
|
||||||
..e
|
..e
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
VdfError::WrongEntryType(e) => WrongEventTypeError {
|
VdfError::WrongEventType(e) => WrongEventTypeError {
|
||||||
src: source.into(),
|
src: source.into(),
|
||||||
err_span: span.into(),
|
err_span: span.into(),
|
||||||
..*e
|
..*e
|
||||||
|
|
|
||||||
46
src/event.rs
46
src/event.rs
|
|
@ -60,6 +60,17 @@ pub enum Event<'a> {
|
||||||
|
|
||||||
/// An entry.
|
/// An entry.
|
||||||
Entry(EntryEvent<'a>),
|
Entry(EntryEvent<'a>),
|
||||||
|
|
||||||
|
/// An additional value for the previous entry.
|
||||||
|
ValueContinuation(ValueContinuationEvent<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum EventType {
|
||||||
|
GroupStart,
|
||||||
|
GroupEnd,
|
||||||
|
Entry,
|
||||||
|
ValueContinuation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -87,6 +98,9 @@ impl<'a> TryFrom<Event<'a>> for GroupStartEvent<'a> {
|
||||||
Err(WrongEventTypeError::new(event, "group start", "group end").into())
|
Err(WrongEventTypeError::new(event, "group start", "group end").into())
|
||||||
}
|
}
|
||||||
Event::Entry(_) => Err(WrongEventTypeError::new(event, "group start", "entry").into()),
|
Event::Entry(_) => Err(WrongEventTypeError::new(event, "group start", "entry").into()),
|
||||||
|
Event::ValueContinuation(_) => {
|
||||||
|
Err(WrongEventTypeError::new(event, "group start", "value continuation").into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +120,9 @@ impl<'a> TryFrom<Event<'a>> for GroupEndEvent {
|
||||||
Err(WrongEventTypeError::new(event, "group end", "group start").into())
|
Err(WrongEventTypeError::new(event, "group end", "group start").into())
|
||||||
}
|
}
|
||||||
Event::Entry(_) => Err(WrongEventTypeError::new(event, "group start", "entry").into()),
|
Event::Entry(_) => Err(WrongEventTypeError::new(event, "group start", "entry").into()),
|
||||||
|
Event::ValueContinuation(_) => {
|
||||||
|
Err(WrongEventTypeError::new(event, "group start", "value continuation").into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +154,24 @@ impl<'a> TryFrom<Event<'a>> for EntryEvent<'a> {
|
||||||
Event::GroupStart(_) => {
|
Event::GroupStart(_) => {
|
||||||
Err(WrongEventTypeError::new(event, "entry", "group start").into())
|
Err(WrongEventTypeError::new(event, "entry", "group start").into())
|
||||||
}
|
}
|
||||||
|
Event::ValueContinuation(_) => {
|
||||||
|
Err(WrongEventTypeError::new(event, "entry", "value continuation").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct ValueContinuationEvent<'a> {
|
||||||
|
pub value: Item<'a>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueContinuationEvent<'_> {
|
||||||
|
pub fn into_owned(self) -> ValueContinuationEvent<'static> {
|
||||||
|
ValueContinuationEvent {
|
||||||
|
value: self.value.into_owned(),
|
||||||
|
span: self.span,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +183,7 @@ impl Event<'_> {
|
||||||
Event::GroupStart(GroupStartEvent { span, .. }) => span.clone(),
|
Event::GroupStart(GroupStartEvent { span, .. }) => span.clone(),
|
||||||
Event::GroupEnd(GroupEndEvent { span, .. }) => span.clone(),
|
Event::GroupEnd(GroupEndEvent { span, .. }) => span.clone(),
|
||||||
Event::Entry(EntryEvent { span, .. }) => span.clone(),
|
Event::Entry(EntryEvent { span, .. }) => span.clone(),
|
||||||
|
Event::ValueContinuation(ValueContinuationEvent { span, .. }) => span.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn into_owned(self) -> Event<'static> {
|
pub fn into_owned(self) -> Event<'static> {
|
||||||
|
|
@ -155,6 +191,16 @@ impl Event<'_> {
|
||||||
Event::GroupStart(event) => Event::GroupStart(event.into_owned()),
|
Event::GroupStart(event) => Event::GroupStart(event.into_owned()),
|
||||||
Event::GroupEnd(event) => Event::GroupEnd(event),
|
Event::GroupEnd(event) => Event::GroupEnd(event),
|
||||||
Event::Entry(event) => Event::Entry(event.into_owned()),
|
Event::Entry(event) => Event::Entry(event.into_owned()),
|
||||||
|
Event::ValueContinuation(event) => Event::ValueContinuation(event.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ty(&self) -> EventType {
|
||||||
|
match self {
|
||||||
|
Event::GroupStart(GroupStartEvent { .. }) => EventType::GroupStart,
|
||||||
|
Event::GroupEnd(GroupEndEvent { .. }) => EventType::GroupEnd,
|
||||||
|
Event::Entry(EntryEvent { .. }) => EventType::Entry,
|
||||||
|
Event::ValueContinuation(ValueContinuationEvent { .. }) => EventType::ValueContinuation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
use super::{Result, Token};
|
use super::{Result, Token};
|
||||||
use crate::error::{NoValidTokenError, UnexpectedTokenError};
|
use crate::error::{NoValidTokenError, UnexpectedTokenError};
|
||||||
use crate::event::{EntryEvent, Event, GroupEndEvent, GroupStartEvent, Item};
|
use crate::event::{
|
||||||
|
EntryEvent, Event, EventType, GroupEndEvent, GroupStartEvent, Item, ValueContinuationEvent,
|
||||||
|
};
|
||||||
use logos::{Lexer, Logos, Span, SpannedIter};
|
use logos::{Lexer, Logos, Span, SpannedIter};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// A VDF token reader.
|
/// A VDF token reader.
|
||||||
pub struct Reader<'a> {
|
pub struct Reader<'a> {
|
||||||
pub source: &'a str,
|
pub source: &'a str,
|
||||||
|
pub last_event: Option<EventType>,
|
||||||
lexer: SpannedIter<'a, Token>,
|
lexer: SpannedIter<'a, Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,6 +17,7 @@ impl<'a> From<&'a str> for Reader<'a> {
|
||||||
fn from(content: &'a str) -> Self {
|
fn from(content: &'a str) -> Self {
|
||||||
Reader {
|
Reader {
|
||||||
source: content,
|
source: content,
|
||||||
|
last_event: None,
|
||||||
lexer: Lexer::new(content).spanned(),
|
lexer: Lexer::new(content).spanned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,8 +33,16 @@ impl<'a> Reader<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next event, this does copies.
|
/// Get the next event, this does copies.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn event(&mut self) -> Option<Result<Event<'a>>> {
|
pub fn event(&mut self) -> Option<Result<Event<'a>>> {
|
||||||
|
let result = self.event_inner();
|
||||||
|
if let Some(Ok(event)) = &result {
|
||||||
|
self.last_event = Some(event.ty());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn event_inner(&mut self) -> Option<Result<Event<'a>>> {
|
||||||
const VALID_KEY: &[Token] = &[
|
const VALID_KEY: &[Token] = &[
|
||||||
Token::Item,
|
Token::Item,
|
||||||
Token::QuotedItem,
|
Token::QuotedItem,
|
||||||
|
|
@ -39,6 +51,8 @@ impl<'a> Reader<'a> {
|
||||||
Token::QuotedStatement,
|
Token::QuotedStatement,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let whitespace_start = self.span().end;
|
||||||
|
|
||||||
let key = match self.token() {
|
let key = match self.token() {
|
||||||
None => {
|
None => {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -86,6 +100,21 @@ impl<'a> Reader<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let whitespace_end = self.span().start;
|
||||||
|
let skipped_newline = self.source[whitespace_start..whitespace_end].contains('\n');
|
||||||
|
let last_event_has_value = matches!(
|
||||||
|
self.last_event,
|
||||||
|
Some(EventType::Entry | EventType::ValueContinuation)
|
||||||
|
);
|
||||||
|
|
||||||
|
// multiple values on the same line create an array
|
||||||
|
if last_event_has_value && !skipped_newline {
|
||||||
|
return Some(Ok(Event::ValueContinuation(ValueContinuationEvent {
|
||||||
|
value: key,
|
||||||
|
span: self.span(),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
const VALID_VALUE: &[Token] = &[
|
const VALID_VALUE: &[Token] = &[
|
||||||
Token::Item,
|
Token::Item,
|
||||||
Token::QuotedItem,
|
Token::QuotedItem,
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,7 @@ fn test_serde_table(path: &str) {
|
||||||
fn test_serde_from_table(path: &str) {
|
fn test_serde_from_table(path: &str) {
|
||||||
let raw = read_to_string(path).unwrap();
|
let raw = read_to_string(path).unwrap();
|
||||||
let result = Table::load_from_str(&raw).unwrap();
|
let result = Table::load_from_str(&raw).unwrap();
|
||||||
|
dbg!(&result);
|
||||||
|
|
||||||
let material: Expected = from_entry(result.into()).expect("table to material");
|
let material: Expected = from_entry(result.into()).expect("table to material");
|
||||||
insta::assert_ron_snapshot!(format!("table_to_material__{}", path), material);
|
insta::assert_ron_snapshot!(format!("table_to_material__{}", path), material);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ expression: parsed
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"Resource/specificPanel.res": {
|
"Resource/specificPanel.res": {
|
||||||
"$envmaptint": ".5",
|
"$envmaptint": [
|
||||||
".5": ".5",
|
".5",
|
||||||
|
".5",
|
||||||
|
".5",
|
||||||
|
],
|
||||||
"\\\\\"$translucent\"": "1",
|
"\\\\\"$translucent\"": "1",
|
||||||
"array": [
|
"array": [
|
||||||
"1",
|
"1",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,5 @@ r#Resource/specificPanel.res(
|
||||||
],
|
],
|
||||||
windows_path: "C:\\test\\no newline",
|
windows_path: "C:\\test\\no newline",
|
||||||
r#\\"$translucent": true,
|
r#\\"$translucent": true,
|
||||||
r#$envmaptint: 0.5,
|
r#$envmaptint: (0.5, 0.5, 0.5),
|
||||||
r#.5: 0.5,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,5 @@ Types(
|
||||||
single: 1.2,
|
single: 1.2,
|
||||||
triple: (1.2, 1.3, 1.4),
|
triple: (1.2, 1.3, 1.4),
|
||||||
single_int: 2.0,
|
single_int: 2.0,
|
||||||
|
another_tuple: (8, "foo", false),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue