mirror of
https://github.com/Ed94/metadesk.git
synced 2026-07-09 11:21:37 -07:00
[namespaces] Type versioning sample.
This commit is contained in:
@@ -12,6 +12,7 @@ cl %compile_flags% ..\samples\static_site_generator\static_site_generator.c
|
||||
cl %compile_flags% ..\samples\output_parse\output_parse.c
|
||||
cl %compile_flags% ..\samples\c_code_generation.c
|
||||
cl %compile_flags% ..\samples\node_errors\node_errors.c
|
||||
cl %compile_flags% ..\samples\namespaced_types\namespaced_types.c
|
||||
echo.
|
||||
echo ~~~ Build All Tests ~~~
|
||||
cl %compile_flags% ..\tests\sanity_tests.c
|
||||
@@ -63,3 +64,12 @@ echo ~~~ Running Error Generation Sample ~~~
|
||||
pushd build
|
||||
node_errors.exe %~dp0\samples\node_errors\node_errors.md
|
||||
popd
|
||||
|
||||
echo.
|
||||
echo ~~~ Running Namespace Type Versioning Sample ~~~
|
||||
pushd build
|
||||
namespaced_types.exe ..\samples\namespaced_types\spec.md ..\samples\namespaced_types\generated\converter.c
|
||||
cl %compile_flags% ..\samples\namespaced_types\conversion_test.c
|
||||
conversion_test.exe
|
||||
popd
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ $CC $compile_flags ../samples/static_site_generator/static_site_generator.c -o s
|
||||
$CC $compile_flags ../samples/output_parse/output_parse.c -o output_parse
|
||||
$CC $compile_flags ../samples/c_code_generation.c -o c_code_generation
|
||||
$CC $compile_flags ../samples/node_errors/node_errors.c -o node_errors
|
||||
$CC $compile_flags ../samples/namespaced_types/namespaced_types.c -o namespaced_types
|
||||
echo
|
||||
echo ~~~ Build All Tests ~~~
|
||||
$CC $compile_flags ../tests/sanity_tests.c -o sanity_tests
|
||||
@@ -63,3 +64,12 @@ echo ~~~ Running Error Generation Sample ~~~
|
||||
pushd build
|
||||
./node_errors ../samples/node_errors/node_errors.md
|
||||
popd
|
||||
|
||||
echo
|
||||
echo ~~~ Running Namespace Type Versioning Sample ~~~
|
||||
pushd build
|
||||
./namespaced_types ../samples/namespaced_types/spec.md ../samples/namespaced_types/generated/converter.c
|
||||
$CC $compile_flags ../samples/namespaced_types/conversion_test.c -o conversion_test
|
||||
./conversion_test
|
||||
popd
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ clang %compile_flags% ..\samples\static_site_generator\static_site_generator.c -
|
||||
clang %compile_flags% ..\samples\output_parse\output_parse.c -o output_parse.exe
|
||||
clang %compile_flags% ..\samples\c_code_generation.c -o c_code_generation.exe
|
||||
clang %compile_flags% ..\samples\node_errors\node_errors.c -o node_errors.exe
|
||||
clang %compile_flags% ..\samples\namespaced_types\namespaced_types.c -o namespaced_types.exe
|
||||
echo.
|
||||
echo ~~~ Build All Tests ~~~
|
||||
clang %compile_flags% ..\tests\sanity_tests.c -o sanity_tests.exe
|
||||
@@ -65,3 +66,13 @@ echo ~~~ Running Error Generation Sample ~~~
|
||||
pushd build
|
||||
node_errors.exe %~dp0\samples\node_errors\node_errors.md
|
||||
popd
|
||||
|
||||
echo.
|
||||
echo ~~~ Running Namespace Type Versioning Sample ~~~
|
||||
pushd build
|
||||
namespaced_types.exe ..\samples\namespaced_types\spec.md ..\samples\namespaced_types\generated\converter.c
|
||||
clang %compile_flags% ..\samples\namespaced_types\conversion_test.c -o conversion_test.exe
|
||||
conversion_test.exe
|
||||
popd
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include "generated/converter.c"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
v1_Entry v1_entry = {0};
|
||||
|
||||
v1_entry.to_remove = 12;
|
||||
|
||||
v1_entry.kind = v1_EntryKind_C;
|
||||
|
||||
v1_entry.flags = v1_EntryFlag_B | v1_EntryFlag_C;
|
||||
|
||||
v1_entry.color.components.r = 255;
|
||||
v1_entry.color.components.g = 128;
|
||||
v1_entry.color.components.b = 1;
|
||||
v1_entry.color.components.a = 42;
|
||||
|
||||
v1_entry.p.x = 2;
|
||||
v1_entry.p.y = 3;
|
||||
|
||||
Entry entry = EntryFromV1(v1_entry);
|
||||
|
||||
assert(entry.kind == EntryKind_C);
|
||||
assert(entry.flags == (EntryFlag_B | EntryFlag_C));
|
||||
assert(entry.color.components.r == 255);
|
||||
assert(entry.color.components.g == 128);
|
||||
assert(entry.color.components.b == 1);
|
||||
assert(entry.color.components.a == 42);
|
||||
assert(v1_entry.p.x == 2);
|
||||
assert(v1_entry.p.y == 3);
|
||||
|
||||
printf("Conversion test success!\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
#include "md.h"
|
||||
#include "md.c"
|
||||
|
||||
static MD_Map MapFromChildren(MD_Node *node)
|
||||
{
|
||||
MD_Map result = {0};
|
||||
for(MD_EachNodeRef(child, node->first_child))
|
||||
{
|
||||
MD_StringMap_Insert(&result, MD_MapCollisionRule_Chain, child->string, child);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void OutputType_C_LHS_Namespace(FILE *file, MD_Map *user_defined_types, MD_String8 prefix, MD_Expr *type)
|
||||
{
|
||||
switch (type->kind)
|
||||
{
|
||||
case MD_ExprKind_Atom:
|
||||
{
|
||||
MD_Node *node = type->node;
|
||||
|
||||
if(MD_StringMap_Lookup(user_defined_types, type->node->string))
|
||||
{
|
||||
fprintf(file, "%.*s", MD_StringExpand(prefix));
|
||||
}
|
||||
fprintf(file, "%.*s", MD_StringExpand(node->whole_string));
|
||||
}break;
|
||||
|
||||
case MD_ExprKind_Pointer:
|
||||
{
|
||||
OutputType_C_LHS_Namespace(file, user_defined_types, prefix, type->sub[0]);
|
||||
if (_MD_OutputType_C_NeedsParens(type))
|
||||
{
|
||||
fprintf(file, "(");
|
||||
}
|
||||
fprintf(file, "*");
|
||||
}break;
|
||||
|
||||
case MD_ExprKind_Array:
|
||||
{
|
||||
OutputType_C_LHS_Namespace(file, user_defined_types, prefix, type->sub[0]);
|
||||
if (_MD_OutputType_C_NeedsParens(type))
|
||||
{
|
||||
fprintf(file, "(");
|
||||
}
|
||||
}break;
|
||||
|
||||
case MD_ExprKind_Volatile: { fprintf(file, "volatile "); }break;
|
||||
case MD_ExprKind_Const: { fprintf(file, "const "); }break;
|
||||
|
||||
default:
|
||||
{
|
||||
fprintf(file, "{ unexpected MD_ExprKind (%i) in type info for node \"%.*s\" }",
|
||||
type->kind,
|
||||
MD_StringExpand(type->node->whole_string));
|
||||
}break;
|
||||
}
|
||||
}
|
||||
|
||||
static void OutputPrefixedType(FILE *f, MD_Map *user_defined_types, MD_String8 prefix, MD_Node *node, MD_u32 depth)
|
||||
{
|
||||
#define I() fprintf(f, "%*s", 4*depth, "");
|
||||
if(MD_NodeHasTag(node, MD_S8Lit("enum")))
|
||||
{
|
||||
I(); fprintf(f, "typedef enum\n");
|
||||
I(); fprintf(f, "{\n");
|
||||
for(MD_EachNode(enumerand, node->first_child))
|
||||
{
|
||||
I(); fprintf(f, " %.*s%.*s_%.*s,\n", MD_StringExpand(prefix),
|
||||
MD_StringExpand(node->string), MD_StringExpand(enumerand->string));
|
||||
}
|
||||
I(); fprintf(f, "} %.*s%.*s;\n\n", MD_StringExpand(prefix), MD_StringExpand(node->string));
|
||||
}
|
||||
else if(MD_NodeHasTag(node, MD_S8Lit("flags")))
|
||||
{
|
||||
I(); fprintf(f, "typedef enum\n");
|
||||
I(); fprintf(f, "{\n");
|
||||
|
||||
MD_String8 singular_flag_type_name = node->string;
|
||||
if(MD_StringMatch(MD_StringSuffix(singular_flag_type_name, 5), MD_S8Lit("Flags"),
|
||||
MD_StringMatchFlag_CaseInsensitive))
|
||||
{
|
||||
singular_flag_type_name = MD_StringChop(singular_flag_type_name, 1);
|
||||
}
|
||||
MD_u32 v = 0;
|
||||
for(MD_EachNode(enumerand, node->first_child))
|
||||
{
|
||||
I(); fprintf(f, " %.*s%.*s_%.*s = (1<<%d),\n", MD_StringExpand(prefix),
|
||||
MD_StringExpand(singular_flag_type_name), MD_StringExpand(enumerand->string), v);
|
||||
v += 1;
|
||||
}
|
||||
I(); fprintf(f, "} %.*s%.*s;\n\n", MD_StringExpand(prefix), MD_StringExpand(node->string));
|
||||
}
|
||||
else if(MD_NodeHasTag(node, MD_S8Lit("union")) || MD_NodeHasTag(node, MD_S8Lit("struct")))
|
||||
{
|
||||
MD_String8 aggregate_kind = MD_NodeHasTag(node, MD_S8Lit("union")) ? MD_S8Lit("union") : MD_S8Lit("struct");
|
||||
|
||||
if(depth == 0)
|
||||
{
|
||||
fprintf(f, "typedef %.*s %.*s%.*s %.*s%.*s;\n",
|
||||
MD_StringExpand(aggregate_kind),
|
||||
MD_StringExpand(prefix), MD_StringExpand(node->string),
|
||||
MD_StringExpand(prefix), MD_StringExpand(node->string));
|
||||
}
|
||||
|
||||
if(depth == 0)
|
||||
{
|
||||
I(); fprintf(f, "%.*s %.*s%.*s\n", MD_StringExpand(aggregate_kind),
|
||||
MD_StringExpand(prefix), MD_StringExpand(node->string));
|
||||
}
|
||||
else
|
||||
{
|
||||
I(); fprintf(f, "%.*s\n", MD_StringExpand(aggregate_kind));
|
||||
}
|
||||
|
||||
I(); fprintf(f, "{\n");
|
||||
for(MD_EachNode(member, node->first_child))
|
||||
{
|
||||
OutputPrefixedType(f, user_defined_types, prefix, member, depth+1);
|
||||
}
|
||||
if(depth == 0)
|
||||
{
|
||||
I(); fprintf(f, "};\n\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
I(); fprintf(f, "} %.*s;\n", MD_StringExpand(node->string));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MD_Expr *type = MD_ParseAsType(node->first_child, node->last_child);
|
||||
|
||||
I();
|
||||
OutputType_C_LHS_Namespace(f, user_defined_types, prefix, type);
|
||||
fprintf(f, " %.*s", MD_StringExpand(node->string));
|
||||
MD_OutputType_C_RHS(f, type);
|
||||
|
||||
fprintf(f, ";\n");
|
||||
}
|
||||
#undef I
|
||||
}
|
||||
|
||||
static void AppendConversionCode(FILE *f, MD_Map *user_defined_types, MD_Node *new_element, MD_Node *old_element, MD_String8 member_path)
|
||||
{
|
||||
MD_Map new_element_map = MapFromChildren(new_element);
|
||||
|
||||
if(MD_NodeHasTag(new_element, MD_S8Lit("struct")))
|
||||
{
|
||||
MD_String8 extended_member_path = MD_PushStringF("%.*s.%.*s", MD_StringExpand(member_path),
|
||||
MD_StringExpand(old_element->string));
|
||||
|
||||
for(MD_EachNode(member, old_element->first_child))
|
||||
{
|
||||
MD_MapSlot *slot = MD_StringMap_Lookup(&new_element_map, member->string);
|
||||
if(slot)
|
||||
{
|
||||
AppendConversionCode(f, user_defined_types, member, slot->value, extended_member_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(f, " // TODO: What to do with %.*s\n", MD_StringExpand(old_element->string));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(MD_NodeIsNil(new_element->first_tag))
|
||||
{
|
||||
if(MD_ChildCountFromNode(new_element) == 1)
|
||||
{
|
||||
MD_String8 type_name = new_element->first_child->string;
|
||||
if(MD_NodeDeepMatch(new_element, old_element, 0, 0))
|
||||
{
|
||||
MD_String8 extended_member_path = MD_PushStringF("%.*s.%.*s", MD_StringExpand(member_path),
|
||||
MD_StringExpand(old_element->string));
|
||||
|
||||
if(MD_StringMap_Lookup(user_defined_types, type_name))
|
||||
{
|
||||
fprintf(f, " result%.*s = %.*sFromV1(v%.*s);\n",
|
||||
MD_StringExpand(extended_member_path), MD_StringExpand(type_name),
|
||||
MD_StringExpand(extended_member_path));
|
||||
}
|
||||
else
|
||||
{ // NOTE(mal): Assumes unknown types are base types
|
||||
fprintf(f, " result%.*s = v%.*s;\n",
|
||||
MD_StringExpand(extended_member_path), MD_StringExpand(extended_member_path));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(f, " // TODO: What to do with %.*s %.*s\n",
|
||||
MD_StringExpand(old_element->string), MD_StringExpand(type_name));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(f, " // TODO: What to do with %.*s\n", MD_StringExpand(old_element->string));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Missing code to process tag @%.*s\n", MD_StringExpand(new_element->first_tag->string));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argument_count, char **arguments)
|
||||
{
|
||||
if(argument_count != 3)
|
||||
{
|
||||
fprintf(stderr, "USAGE: %s spec_file_name.md out_file_name.c\n", arguments[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
MD_ParseResult spec = MD_ParseWholeFile(MD_S8CString(arguments[1]));
|
||||
if(spec.first_error)
|
||||
{
|
||||
for(MD_Error *error = spec.first_error; error; error = error->next)
|
||||
{
|
||||
MD_NodeMessage(error->node, error->kind, error->string);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(arguments[2], "wb");
|
||||
if(f == 0)
|
||||
{
|
||||
fprintf(stderr, "Unable to open destination file \"%s\"\n", arguments[2]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
MD_Map ns_map = MapFromChildren(spec.namespaces);
|
||||
|
||||
// NOTE(mal): Old types get "v1_" as a prefix
|
||||
fprintf(f, "// V1\n");
|
||||
MD_Node *v1 = MD_StringMap_Lookup(&ns_map, MD_S8Lit("v1"))->value;
|
||||
MD_Map v1_map = MapFromChildren(v1);
|
||||
for(MD_EachNodeRef(node, v1->first_child))
|
||||
{
|
||||
OutputPrefixedType(f, &v1_map, MD_S8Lit("v1_"), node, 0);
|
||||
}
|
||||
|
||||
// NOTE(mal): New types don't get a prefix
|
||||
fprintf(f, "// V2\n");
|
||||
MD_Map empty_map = {0};
|
||||
|
||||
MD_Node *v2 = MD_StringMap_Lookup(&ns_map, MD_S8Lit("v2"))->value;
|
||||
for(MD_EachNodeRef(node, v2->first_child))
|
||||
{
|
||||
OutputPrefixedType(f, &empty_map, MD_S8Lit(""), node, 0);
|
||||
}
|
||||
|
||||
// NOTE(mal): Routines that map old into new
|
||||
fprintf(f, "// V1->V2\n");
|
||||
MD_Map v2_map = MapFromChildren(v2);
|
||||
for(MD_EachNodeRef(node, v1->first_child))
|
||||
{
|
||||
MD_MapSlot *slot = MD_StringMap_Lookup(&v2_map, node->string);
|
||||
MD_Node *v2_type = slot->value;
|
||||
MD_Map children_map = MapFromChildren(v2_type);
|
||||
|
||||
fprintf(f, "static %.*s %.*sFromV1(v1_%.*s v)\n{\n",
|
||||
MD_StringExpand(node->string), MD_StringExpand(node->string), MD_StringExpand(node->string));
|
||||
|
||||
if(MD_NodeHasTag(node, MD_S8Lit("enum")))
|
||||
{
|
||||
fprintf(f, " %.*s result = 0;\n", MD_StringExpand(node->string));
|
||||
fprintf(f, " switch(v)\n {\n");
|
||||
|
||||
for(MD_EachNode(enumerand, node->first_child))
|
||||
{
|
||||
fprintf(f, " case v1_%.*s_%.*s: ",
|
||||
MD_StringExpand(node->string), MD_StringExpand(enumerand->string));
|
||||
MD_MapSlot *slot = MD_StringMap_Lookup(&children_map, enumerand->string);
|
||||
if(slot)
|
||||
{
|
||||
MD_Node *v2_enumerand = slot->value;
|
||||
fprintf(f, "result = %.*s_%.*s; break;\n",
|
||||
MD_StringExpand(node->string), MD_StringExpand(v2_enumerand->string));
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(f, "assert(!\"Enumerand v1_%.*s_%.*s is no longer allowed\\n\");\n",
|
||||
MD_StringExpand(node->string), MD_StringExpand(enumerand->string));
|
||||
}
|
||||
}
|
||||
fprintf(f, " default: assert(!\"Illegal value for enum v1_%.*s\\n\"); break;\n", MD_StringExpand(node->string));
|
||||
fprintf(f, " }\n");
|
||||
}
|
||||
else if(MD_NodeHasTag(node, MD_S8Lit("flags")))
|
||||
{
|
||||
fprintf(f, " %.*s result = 0;\n", MD_StringExpand(node->string));
|
||||
|
||||
MD_String8 singular_flag_type_name = node->string;
|
||||
if(MD_StringMatch(MD_StringSuffix(singular_flag_type_name, 5), MD_S8Lit("Flags"),
|
||||
MD_StringMatchFlag_CaseInsensitive))
|
||||
{
|
||||
singular_flag_type_name = MD_StringChop(singular_flag_type_name, 1);
|
||||
}
|
||||
|
||||
for(MD_EachNode(flag, node->first_child))
|
||||
{
|
||||
fprintf(f, " if(v & v1_%.*s_%.*s) ",
|
||||
MD_StringExpand(singular_flag_type_name), MD_StringExpand(flag->string));
|
||||
MD_MapSlot *slot = MD_StringMap_Lookup(&children_map, flag->string);
|
||||
if(slot)
|
||||
{
|
||||
fprintf(f, "result |= %.*s_%.*s;\n",
|
||||
MD_StringExpand(singular_flag_type_name), MD_StringExpand(flag->string));
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(f, "assert(!\"Flag v1_%.*s is no longer allowed\\n\");\n", MD_StringExpand(flag->string));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(MD_NodeHasTag(node, MD_S8Lit("union")))
|
||||
{
|
||||
fprintf(f, " %.*s result = {0};\n", MD_StringExpand(node->string));
|
||||
|
||||
MD_Node *authoritative_member = v2_type->first_child;
|
||||
for(MD_EachNode(member, v2_type->first_child))
|
||||
{
|
||||
if(MD_NodeHasTag(member, MD_S8Lit("authoritative")))
|
||||
{
|
||||
authoritative_member = member;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MD_Map v1_children_map = MapFromChildren(node);
|
||||
MD_MapSlot *v1_member_slot = MD_StringMap_Lookup(&v1_children_map, authoritative_member->string);
|
||||
if(v1_member_slot)
|
||||
{
|
||||
AppendConversionCode(f, &v2_map, authoritative_member, v1_member_slot->value, MD_S8Lit(""));
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Can't generate conversion for union %.*s", MD_StringExpand(node->string));
|
||||
}
|
||||
}
|
||||
else if(MD_NodeHasTag(node, MD_S8Lit("struct")))
|
||||
{
|
||||
fprintf(f, " %.*s result = {0};\n", MD_StringExpand(node->string));
|
||||
MD_Map v1_children_map = MapFromChildren(node);
|
||||
for(MD_EachNode(member, v2_type->first_child))
|
||||
{
|
||||
MD_MapSlot *v1_member_slot = MD_StringMap_Lookup(&v1_children_map, member->string);
|
||||
if(v1_member_slot)
|
||||
{
|
||||
AppendConversionCode(f, &v2_map, member, v1_member_slot->value, MD_S8Lit(""));
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Can't generate conversion for struct member %.*s", MD_StringExpand(node->string));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Missing code to process tag @%.*s\n", MD_StringExpand(node->first_tag->string));
|
||||
return -1;
|
||||
}
|
||||
fprintf(f, " return result;\n");
|
||||
fprintf(f, "}\n\n");
|
||||
fflush(f);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#namespace v1
|
||||
@enum EntryKind : { A, B, C, E, }
|
||||
@flags EntryFlags : { A, B, C, }
|
||||
|
||||
@union Color : {
|
||||
@struct components : {
|
||||
r : uint8_t;
|
||||
g : uint8_t;
|
||||
b : uint8_t;
|
||||
a : uint8_t;
|
||||
};
|
||||
raw : ([4]uint8_t);
|
||||
}
|
||||
|
||||
@struct Entry : {
|
||||
to_remove : uint16_t;
|
||||
kind : EntryKind;
|
||||
flags : EntryFlags;
|
||||
color : Color;
|
||||
@struct p : {
|
||||
x : float;
|
||||
y : float;
|
||||
}
|
||||
}
|
||||
|
||||
#namespace v2
|
||||
@enum EntryKind : { A, B, B2, C, }
|
||||
@flags EntryFlags : { B, C, D, }
|
||||
|
||||
@union Color : {
|
||||
// NOTE(mal): In the general case, it's impossible to decide automatically which member should be used
|
||||
// to port data across versions of a union. That's why I've introduced @authoritative here
|
||||
@authoritative
|
||||
@struct components : {
|
||||
b : uint8_t;
|
||||
g : uint8_t;
|
||||
r : uint8_t;
|
||||
a : uint8_t;
|
||||
}
|
||||
raw : ([4]uint8_t);
|
||||
}
|
||||
|
||||
@struct Entry : {
|
||||
kind : EntryKind;
|
||||
color : Color;
|
||||
@struct p : {
|
||||
x : float;
|
||||
y : float;
|
||||
z : float;
|
||||
};
|
||||
flags : EntryFlags;
|
||||
}
|
||||
Reference in New Issue
Block a user