From: Brendan Hansen Date: Wed, 2 Nov 2022 14:46:37 +0000 (-0500) Subject: moved templating library to new package X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=483fc4d4faf7b75ac31eece70ce1041a410706d4;p=onyxlang.io.git moved templating library to new package --- diff --git a/build.onyx b/build.onyx index 68a9c01..75739eb 100644 --- a/build.onyx +++ b/build.onyx @@ -1,8 +1,9 @@ #load "./lib/http-server/module" #load "./lib/postgres/module" #load "./lib/postgres-orm/module" +#load "./lib/otmp/module" #library_path "./bin" #load_all "./src" -#load "./src/html-templates/module" +// #load "./src/html-templates/module" diff --git a/onyx-pkg.ini b/onyx-pkg.ini index c6f22aa..b34c390 100644 --- a/onyx-pkg.ini +++ b/onyx-pkg.ini @@ -8,6 +8,9 @@ version=0.0.1 [config] lib_source_directory=./lib lib_bin_directory=./bin +run_cmd=onyx run build +debug_cmd=onyx run --debug build +test_cmd= [native_library] build_cmd= @@ -16,6 +19,7 @@ library= [dependencies] git://onyxlang.io/repo/http-server=0.0.17 git://onyxlang.io/repo/postgres-orm=0.0.18 +git://onyxlang.io/repo/otmp=0.0.2 [dependency_folders] git://onyxlang.io/repo/http-server=http-server @@ -23,4 +27,5 @@ git://onyxlang.io/repo/postgres-orm=postgres-orm git://onyxlang.io/repo/json=json git://onyxlang.io/repo/base64=base64 git://onyxlang.io/repo/postgres=postgres +git://onyxlang.io/repo/otmp=otmp diff --git a/src/app.onyx b/src/app.onyx index 9baa098..461cfe2 100644 --- a/src/app.onyx +++ b/src/app.onyx @@ -22,6 +22,11 @@ reg: otmp.TemplateRegistry; .[ 9, 10, 11, 12 ] ], + matrix2 = .[ + .[ 1, 2 ], + .[ 3, 4 ], + ], + test = req, }); res->status(200); @@ -38,9 +43,7 @@ main :: () { #if #defined(runtime.vars.Debug) { app->pipe((req, res) => { - // printf("Before: {}\n", alloc.heap.get_watermark()); reg->refresh_templates(); - // printf("After: {}\n", alloc.heap.get_watermark()); }); } diff --git a/src/html-templates/TODO.md b/src/html-templates/TODO.md deleted file mode 100644 index 3c71e3b..0000000 --- a/src/html-templates/TODO.md +++ /dev/null @@ -1,4 +0,0 @@ - - -- [ ] // :EscapedStrings - diff --git a/src/html-templates/module.onyx b/src/html-templates/module.onyx deleted file mode 100644 index 2230b9b..0000000 --- a/src/html-templates/module.onyx +++ /dev/null @@ -1,5 +0,0 @@ -package otmp - -#load_all "./src" - - diff --git a/src/html-templates/src/otmp.onyx b/src/html-templates/src/otmp.onyx deleted file mode 100644 index e1f57da..0000000 --- a/src/html-templates/src/otmp.onyx +++ /dev/null @@ -1,155 +0,0 @@ -package otmp - -use core.alloc {as_allocator, arena} -use core {array, io, os, string, tprintf, printf} - -// -// Template Registry -// - -registry :: () -> TemplateRegistry { - t: TemplateRegistry; - t.arena = arena.make(context.allocator, 64 * 1024); - t.templates->init(); - - return t; -} - -#overload -delete :: (t: ^TemplateRegistry) { - for ^temp: t.templates.entries { - delete(temp.value); - } - - delete(^t.templates); - arena.free(^t.arena); -} - -#inject TemplateRegistry { - load_directory :: (self: ^TemplateRegistry, dir: str, extension: str) { - for os.list_directory(dir) { - if string.ends_with(it->name(), extension) { - name := it->name(); - name = name[0 .. string.last_index_of(name, #char ".")]; - - self->load_template(name, tprintf("{}/{}", dir, it->name())); - } - - if it.type == .Directory { - self->load_directory(tprintf("{}/{}", dir, it->name()), extension); - } - } - } - - load_template :: (self: ^TemplateRegistry, name: str, filename: str) -> Error { - printf("Loading template {} from {}.\n", name, filename); - if self.templates->has(name) { - printf("[ERROR] Template '{}' already exists.\n"); - return .Duplicate_Template; - } - - if !os.is_file(filename) { - return .Template_Not_Found; - } - - contents := os.get_contents(filename); - stored_contents := contents; - defer delete(^stored_contents); - - permanent_name := string.alloc_copy(name, as_allocator(^self.arena)); - - temp := template_make(as_allocator(^self.arena)); - temp.filepath = filename |> string.alloc_copy(as_allocator(^self.arena)); - temp.name = permanent_name; - - if err := parse_template(temp, ^contents); err != .None { - return .Template_Parse_Error; - } - - self.templates[permanent_name] = temp; - return .None; - } - - refresh_templates :: (self: ^TemplateRegistry) { - for ^temp: self.templates.entries { - array.clear(^temp.value.instructions); - arena.clear(^temp.value.node_storage); - - contents := os.get_contents(temp.value.filepath); - stored_contents := contents; - defer delete(^stored_contents); - - parse_template(temp.value, ^contents); - } - } - - get_template :: (self: ^TemplateRegistry, name: str) -> ^Template { - return self.templates[name]; - } - - render_template :: (self: ^TemplateRegistry, name: str, output: ^io.Writer, scope: any) -> Error { - temp := self.templates[name]; - if temp == null { - return .Template_Not_Found; - } - - tscope, _ := core.misc.any_to_map(scope); - defer delete(^tscope); - - return temp->render(self, ^tscope, output); - } -} - - - - -// -// Template -// -#package -template_make :: (alloc: Allocator) -> ^Template { - t := new(Template, alloc); - t.node_storage = arena.make(context.allocator, 64 * 1024); - return t; -} - -#overload -delete :: (t: ^Template) { - delete(^t.instructions); - arena.free(^t.node_storage); -} - -#inject Template { - render :: (self: ^Template, reg: ^TemplateRegistry, scope: ^TemplateScope, output: ^io.Writer) -> Error { - r := ^TemplateRenderer.{ - t = self, - w = output, - reg = reg, - scope = scope - }; - - err := render_template(r); - - if err != .None { - core.printf("Template Error: {}\n", r.error); - } - - return err; - } -} - - -#package -make_node :: macro (t: ^Template, $T: type_expr) -> ^T where IsTNode(T) { - r := new(T, allocator=as_allocator(^t.node_storage)); - r.type = T; - return r; -} - -#package -make_expr :: macro (t: ^Template, $T: type_expr) -> ^T where IsTExpr(T) { - r := new(T, allocator=as_allocator(^t.node_storage)); - r.type = T; - return r; -} - diff --git a/src/html-templates/src/parser.onyx b/src/html-templates/src/parser.onyx deleted file mode 100644 index 0f5cf26..0000000 --- a/src/html-templates/src/parser.onyx +++ /dev/null @@ -1,513 +0,0 @@ -package otmp - - -use core {string, array, iter, conv} -use core.alloc {as_allocator} - -ParseError :: enum { - None; - Unexpected_Token; - Expected_Token; - - Nested_Command; - - Cannot_Nest_Blocks; -} - - -#package -TemplateLexer :: struct { - // The input string - s: ^str; - line: u32; - col: u32; - - hit_eof := false; - - inside_command := false; - inside_expression := false; - error: ParseError; - - token_buffer: [..] TemplateToken; -} - -TemplateToken :: struct { - Type :: enum { - Error; - EOF; - - Text; // Raw Text - - Command_Start; Command_End; - Expression_Start; Expression_End; - - Keyword_Block; - Keyword_EndBlock; - Keyword_Foreach; - Keyword_EndForeach; - Keyword_In; - Keyword_Extends; - - String_Literal; - Int_Literal; - - Variable; - Symbol; - - Dot; - Open_Bracket; - Close_Bracket; - } - - type: Type; - text: str; - line: u32; - col: u32; -} - -#inject TemplateLexer { - peek :: (self: ^TemplateLexer, n := 0) -> TemplateToken { - while n >= self.token_buffer.length { - next := self->eat_next_token(); - if next.type == .EOF do return next; - - self.token_buffer << next; - } - - return self.token_buffer[n]; - } - - consume :: (self: ^TemplateLexer) -> TemplateToken { - if !array.empty(self.token_buffer) { - tkn := self.token_buffer[0]; - array.delete(^self.token_buffer, 0); - return tkn; - } - - tkn := self->eat_next_token(); - return tkn; - } - - eat_characters :: (self: ^TemplateLexer, chars := 1) -> str { - for chars { - if self.s.data[it] == #char "\n" { - self.line += 1; - self.col = 0; - } - - self.col += 1; - } - - defer string.advance(self.s, chars); - return self.s.data[0 .. chars]; - } - - eat_whitespace :: (self: ^TemplateLexer) { - while !string.empty(*self.s) { - switch self.s.data[0] { - case #char "\n", #char "\t", #char "\r", #char " " { - self->eat_characters(1); - } - - case #default { - break break; - } - } - } - } - - eat_next_token :: (self: ^TemplateLexer) -> TemplateToken { - tkn: TemplateToken; - tkn.line = self.line; - tkn.col = self.col; - - self->eat_whitespace(); - - if string.empty(*self.s) { - self.hit_eof = true; - yield_token(.EOF); - } - - token_match("{{") { - if self.inside_command { - self.error = .Nested_Command; - yield_token(.Error); - } - - self.inside_command = true; - yield_token(.Command_Start); - } - - token_match("}}") { - if self.inside_command { - self.inside_command = false; - } - - yield_token(.Command_End); - } - - token_match("{%") { - if self.inside_expression { - self.error = .Nested_Command; - yield_token(.Error); - } - - self.inside_expression = true; - yield_token(.Expression_Start); - } - - token_match("%}") { - if self.inside_expression { - self.inside_expression = false; - } - - yield_token(.Expression_End); - } - - if self.inside_command || self.inside_expression { - self->eat_whitespace(); - - token_consume("block", .Keyword_Block); - token_consume("endblock", .Keyword_EndBlock); - token_consume("foreach", .Keyword_Foreach); - token_consume("endforeach", .Keyword_EndForeach); - token_consume("in", .Keyword_In); - token_consume("extends", .Keyword_Extends); - token_consume(".", .Dot); - token_consume("[", .Open_Bracket); - token_consume("]", .Close_Bracket); - - if self.s.data[0] == #char "\"" { - // :TODO add escaped strings - self->eat_characters(1); - - index := string.index_of(*self.s, #char "\""); - tkn.text = self->eat_characters(index); - self->eat_characters(1); - - yield_token(.String_Literal); - } - - if self.s.data[0] == #char "$" { - self->eat_characters(1); - - chars := 0; - while chars < self.s.length && self.s.data[chars]->is_alphanum() { - chars += 1; - } - - tkn.text = self->eat_characters(chars); - - yield_token(.Variable); - } - - if self.s.data[0]->is_num() { - chars := 0; - while chars < self.s.length && self.s.data[chars]->is_num() { - chars += 1; - } - - tkn.text = self->eat_characters(chars); - - yield_token(.Int_Literal); - } - - if self.s.data[0]->is_alphanum() { - chars := 0; - while chars < self.s.length && - (self.s.data[chars]->is_alphanum() || self.s.data[chars] == #char "_") { - chars += 1; - } - - tkn.text = self->eat_characters(chars); - - yield_token(.Symbol); - } - - } else { - length := 1; - while self.s.data[length] != #char "{" && length < self.s.length { - length += 1; - } - - tkn.text = self->eat_characters(length); - yield_token(.Text); - } - - yield_token :: macro (kind: TemplateToken.Type) { - tkn.type = kind; - return tkn; - } - - token_match :: macro (t: str, body: Code) { - if string.starts_with(*self.s, t) { - tkn.text = self->eat_characters(t.length); - - #unquote body; - } - } - - token_consume :: macro (t: str, kind: TemplateToken.Type) { - if string.starts_with(*self.s, t) { - tkn.text = self->eat_characters(t.length); - yield_token(kind); - } - } - } -} - -#overload -delete :: (tl: ^TemplateLexer) { - delete(^tl.token_buffer); -} - -#overload -iter.as_iterator :: (tl: ^TemplateLexer) => { - return iter.generator( - ^.{ tl = tl, hit_error = false }, - - (ctx) => { - if !ctx.hit_error { - tkn := ctx.tl->consume(); - if tkn.type == .Error || tkn.type == .EOF { - ctx.hit_error = true; - } - - return tkn, true; - } - - return .{}, false; - } - ); -} - - -#package -TemplateParser :: struct { - t: ^Template; - l: ^TemplateLexer; - - instruction_targets: [..] ^[..] ^TNode; -} - -#package -parse_template :: (t: ^Template, s: ^str) -> ParseError { - l := TemplateLexer.{s}; - p := TemplateParser.{t, ^l}; - p.instruction_targets << ^t.instructions; - defer delete(^p.instruction_targets); - - return parse_statements(^p); -} - -#local -parse_statements :: (use p: ^TemplateParser) -> ParseError { - while !p.l.hit_eof { - switch tkn := p.l->consume(); tkn.type { - case .Command_Start { - if err := parse_statement(p); err != .None { - return err; - } - - expect_token(p, .Command_End); - } - - case .Expression_Start { - if node, err := parse_expression(p); err != .None { - return err; - } else { - *array.get(instruction_targets, -1) << node; - } - - expect_token(p, .Expression_End); - } - - case .Text { - text_node := make_node(t, TNodeText); - text_node.text = string.alloc_copy(tkn.text, as_allocator(^t.node_storage)); - *array.get(instruction_targets, -1) << text_node; - } - } - } - - return .None; -} - -#local -parse_statement :: (use p: ^TemplateParser) -> ParseError { - switch tkn := p.l->consume(); tkn.type { - case .Keyword_Extends { - text, err := parse_string(p); - if err != .None do return err; - - extend_node := make_node(t, TNodeExtend); - extend_node.template_name = text; - - *array.get(instruction_targets, -1) << extend_node; - } - - case .Keyword_Block { - text, err := parse_string(p); - if err != .None do return err; - - block_node := make_node(t, TNodeBlock); - block_node.block_name = text; - - *array.get(instruction_targets, -1) << block_node; - - instruction_targets << ^block_node.contents; - } - - case .Keyword_EndBlock { - array.pop(^instruction_targets); - } - - case .Keyword_Foreach { - var_tkn: TemplateToken; - expect_token(p, .Variable, #(var_tkn)); - - expect_token(p, .Keyword_In); - - iter_tkn: TemplateToken; - expect_token(p, .Variable, #(iter_tkn)); - - var_expr := do { - name := string.alloc_copy(iter_tkn.text, as_allocator(^t.node_storage)); - - var_expr := make_expr(t, TExprVar); - var_expr.var_name = name; - - return var_expr; - }; - - for_node := make_node(t, TNodeForeach); - for_node.var_name = string.alloc_copy(var_tkn.text, as_allocator(^t.node_storage)); - for_node.list = var_expr; - - *array.get(instruction_targets, -1) << for_node; - instruction_targets << ^for_node.body; - } - - case .Keyword_EndForeach { - array.pop(^instruction_targets); - } - } - - return .None; -} - -#local -parse_expression :: (use p: ^TemplateParser) -> (^TExpr, ParseError) { - retval: ^TExpr = null; - err := ParseError.None; - - switch tkn := p.l->consume(); tkn.type { - case .Keyword_Block { - name, err := parse_string(p); - if err != .None do return null, err; - - block_expr := make_expr(t, TExprBlock); - block_expr.block_name = name; - - retval = block_expr; - } - - case .Variable { - name := tkn.text |> string.alloc_copy(as_allocator(^t.node_storage)); - - var_expr := make_expr(t, TExprVar); - var_expr.var_name = name; - - retval = var_expr; - } - - case .Int_Literal { - value: i32 = ~~ conv.str_to_i64(tkn.text); - - int_expr := make_expr(t, TExprInt); - int_expr.val = value; - - retval = int_expr; - } - } - - if retval == null { - err = .Unexpected_Token; - } - - while true do switch tkn := p.l->peek(); tkn.type { - case .Dot { - p.l->consume(); - - if (p.l->peek()).type != .Symbol { - err = .Unexpected_Token; - break break; - } - - sym_tkn := p.l->consume(); - - select_expr := make_expr(t, TExprSelector); - select_expr.var = retval; - select_expr.field = sym_tkn.text |> string.alloc_copy(as_allocator(^t.node_storage)); - - retval = select_expr; - } - - case .Open_Bracket { - p.l->consume(); - - expr, err' := parse_expression(p); - if err != .None do break break; - - subscript_expr := make_expr(t, TExprSubscript); - subscript_expr.var = retval; - subscript_expr.sub = expr; - - retval = subscript_expr; - - if (p.l->peek()).type != .Close_Bracket { - err = .Unexpected_Token; - break break; - } - - p.l->consume(); - } - - case #default do break break; - } - - return retval, err; -} - -#local -parse_string :: (use p: ^TemplateParser) -> (str, ParseError) { - str_tkn := p.l->consume(); - if str_tkn.type != .String_Literal { - return "", .Unexpected_Token; - } - - value := str_tkn.text |> string.alloc_copy(as_allocator(^t.node_storage)); - - return value, .None; -} - -#local -expect_token :: #match #local {} - -#overload -expect_token :: macro (p: ^TemplateParser, type: TemplateToken.Type, out: Code) { - if (p.l->peek()).type != type { - return ParseError.Expected_Token; - } - - (#unquote out) = p.l->consume(); -} - -#overload -expect_token :: macro (p: ^TemplateParser, type: TemplateToken.Type) { - if (p.l->peek()).type != type { - return ParseError.Expected_Token; - } - - p.l->consume(); -} diff --git a/src/html-templates/src/render.onyx b/src/html-templates/src/render.onyx deleted file mode 100644 index 4fd012a..0000000 --- a/src/html-templates/src/render.onyx +++ /dev/null @@ -1,155 +0,0 @@ -package otmp - -use core {io, tprintf} -use core.misc { - any_iter, - any_dereference, - any_selector, - any_subscript -} - -#package -TemplateRenderer :: struct { - t: ^Template; - w: ^io.Writer; - reg: ^TemplateRegistry; - - scope: ^TemplateScope; - blocks: Map(str, ^TNodeBlock); - - error: str; -} - -#package -render_template :: (use r: ^TemplateRenderer) -> Error { - return render_instructions(r, t.instructions); -} - -#local -render_instructions :: (use r: ^TemplateRenderer, instrs: [..] ^TNode) -> Error { - for instrs { - switch it.type { - case TNodeText { - text := cast(^TNodeText) it; - io.write_str(w, text.text); - } - - case TNodeBlock { - block := cast(^TNodeBlock) it; - if blocks->has(block.block_name) { - error = tprintf("Block '{}' defined multiple times."); - return .Render_Error; - } - - blocks[block.block_name] = block; - } - - case TNodeExtend { - extend := cast(^TNodeExtend) it; - template := reg->get_template(extend.template_name); - if template == null { - error = tprintf("Template '{}' not found."); - return .Render_Error; - } - - if err := render_instructions(r, template.instructions); err != .None { - return err; - } - } - - case TNodeForeach { - // :Temporary :TemplateVariables - for_node := cast(^TNodeForeach) it; - var := cast(^TExprVar) for_node.list; - if !(scope->has(var.var_name)) { - continue; - } - - for any_iter(scope->get(var.var_name)) { - scope->put(for_node.var_name, it); - - if err := render_instructions(r, for_node.body); err != .None { - return err; - } - } - - scope->delete(for_node.var_name); - } - - - - case TExprBlock { - block := cast(^TExprBlock) it; - if !(blocks->has(block.block_name)) { - continue; - } - - if err := render_instructions(r, blocks[block.block_name].contents); err != .None { - return err; - } - } - - case TExprVar, TExprSelector, TExprSubscript { - var := resolve_expr_to_any(r, cast(^TExpr) it); - if !var.data do continue; - - io.write_format_va(w, "{}", .[var]); - } - - case #default { - error = tprintf("Unhandled node type '{}'", it.type); - return .Render_Error; - } - } - } - - return .None; -} - -#local -resolve_expr_to_any :: (use r: ^TemplateRenderer, expr: ^TExpr) -> any { - switch expr.type { - case TExprVar { - var := cast(^TExprVar) expr; - // :ErrorHandling if variable does not exist - return scope->get(var.var_name); - } - - case TExprSelector { - selector := cast(^TExprSelector) expr; - - sub_any := resolve_expr_to_any(r, selector.var); - sub_any = any_dereference(sub_any); - return any_selector(sub_any, selector.field); - } - - case TExprSubscript { - subscript := cast(^TExprSubscript) expr; - - sub_any := resolve_expr_to_any(r, subscript.var); - sub := any_to_int(resolve_expr_to_any(r, subscript.sub)); - - return any_subscript(sub_any, ~~ sub); - } - } - - return .{null, void}; -} - - -#local -// :StandardLibrary -any_to_int :: (v: any) -> i64 { - switch v.type { - C(u32); - C(i32); - C(u64); - C(i64); - - C :: macro (T: type_expr) { - case T do return ~~ *cast(^T) v.data; - } - } - - return 0; -} diff --git a/src/html-templates/src/types.onyx b/src/html-templates/src/types.onyx deleted file mode 100644 index a97d166..0000000 --- a/src/html-templates/src/types.onyx +++ /dev/null @@ -1,112 +0,0 @@ -package otmp - -use core.alloc.arena {Arena} - -TemplateRegistry :: struct { - templates: Map(str, ^Template); - - arena: Arena; -} - -Template :: struct { - name: str; - filepath: str; - - instructions: [..] ^TNode; - - node_storage: Arena; -} - - -TNode :: struct { - type: type_expr; -} - -TNodeText :: struct { - use node: TNode; - - text: str; // storage: template.node_storage -} - -TNodeExtend :: struct { - use node: TNode; - - template_name: str; // storage: template.node_storage -} - -TNodeBlock :: struct { - use node: TNode; - - block_name: str; // storage: template.node_storage - contents: [..] ^TNode; -} - -TNodeForeach :: struct { - use node: TNode; - - var_name: str; // storage: template.node_storage - list: ^TExpr; - body: [..] ^TNode; -} - - -TExpr :: struct { - use node: TNode; -} - -TExprBlock :: struct { - use expr: TExpr; - - block_name: str; // storage: template.node_storage -} - -TExprVar :: struct { - use expr: TExpr; - - var_name: str; // storage: template.node_storage -} - -TExprSelector :: struct { - use expr: TExpr; - - var: ^TExpr; - field: str; // storage: template.node_storage -} - -TExprSubscript :: struct { - use expr: TExpr; - - var: ^TExpr; - sub: ^TExpr; -} - -TExprInt :: struct { - use expr: TExpr; - - val: i32; -} - - -Error :: enum { - None; - Template_Not_Found; - Duplicate_Template; - - Template_Parse_Error; - - Render_Error; -} - - -TemplateScope :: #type Map(str, any); - - -#package { - IsTNode :: interface (t: $T) { - { ^t } -> ^TNode; - } - - IsTExpr :: interface (t: $T) { - { ^t } -> ^TExpr; - } -} diff --git a/www/static/css/style.css b/www/static/css/style.css index 1cefa94..f4fc2d0 100644 --- a/www/static/css/style.css +++ b/www/static/css/style.css @@ -2,9 +2,20 @@ padding: 0; margin: 0; box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + * { + background-color: #111; + color: #fff; + } +} - background-color: #111; - color: #fff; +@media (prefers-color-scheme: light) { + * { + background-color: #ffd; + color: #000; + } } diff --git a/www/templates/index.html b/www/templates/index.html index e18027d..460c22e 100644 --- a/www/templates/index.html +++ b/www/templates/index.html @@ -19,17 +19,7 @@ {{endforeach}} - - - {{foreach $row in $matrix}} - - {{foreach $col in $row}} - - {{endforeach}} - - {{endforeach}} - -
{% $col %}
+ {% partial "matrix" $matrix2 $matrix %}

Name: {% $test.headers %}

Age: {% $test.cookies %}

diff --git a/www/templates/matrix.html b/www/templates/matrix.html new file mode 100644 index 0000000..067fd47 --- /dev/null +++ b/www/templates/matrix.html @@ -0,0 +1,25 @@ + + + {{foreach $row in $1}} + + {{foreach $col in $row}} + + {{endforeach}} + + {{endforeach}} + +
Value: {% $col %}
+ + + + + {{foreach $row in $0}} + + {{foreach $col in $row}} + + {{endforeach}} + + {{endforeach}} + +
Value: {% $col %}
+