nattoppet 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +8 -8
- package/LICENSE +21 -0
- package/dist/nattoppet-dev.js +84 -0
- package/{nattoppet-init.ts → dist/nattoppet-init.js} +46 -48
- package/dist/nattoppet-native.js +116 -0
- package/dist/nattoppet.js +87083 -0
- package/package.json +21 -37
- package/readme.md +12 -6
- package/compiler.ts +0 -169
- package/nattoppet-dev.ts +0 -94
- package/nattoppet-native.ts +0 -116
- package/nattoppet.ts +0 -35
- package/stdlib.ts +0 -120
- /package/{common.ymd → dist/common.ymd} +0 -0
- /package/{form.css → dist/form.css} +0 -0
- /package/{form.js → dist/form.js} +0 -0
- /package/{form.ymd → dist/form.ymd} +0 -0
- /package/{katex.css → dist/katex.css} +0 -0
- /package/{katex.ymd → dist/katex.ymd} +0 -0
- /package/{koa.css → dist/koa.css} +0 -0
- /package/{koa.ymd → dist/koa.ymd} +0 -0
- /package/{ppt.css → dist/ppt.css} +0 -0
- /package/{ppt.js → dist/ppt.js} +0 -0
- /package/{ppt.ymd → dist/ppt.ymd} +0 -0
- /package/{tml.css → dist/tml.css} +0 -0
- /package/{tml.js → dist/tml.js} +0 -0
- /package/{tml.ymd → dist/tml.ymd} +0 -0
- /package/{vue.css → dist/vue.css} +0 -0
- /package/{vue.ymd → dist/vue.ymd} +0 -0
package/GUIDE.md
CHANGED
|
@@ -34,8 +34,8 @@ This guide covers writing `.ymd` files from the perspective of an end user.
|
|
|
34
34
|
|
|
35
35
|
### Prerequisites
|
|
36
36
|
|
|
37
|
-
- [
|
|
38
|
-
-
|
|
37
|
+
- [Node.js](https://nodejs.org/) 22.18 or newer.
|
|
38
|
+
- Install Nattoppet with `npm install -g nattoppet`.
|
|
39
39
|
|
|
40
40
|
### Your first document
|
|
41
41
|
|
|
@@ -56,13 +56,13 @@ This line is not indented, so it passes through as raw HTML.
|
|
|
56
56
|
Compile it:
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
|
|
59
|
+
nattoppet input.ymd > output.html
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Preview with live reload:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
|
|
65
|
+
nattoppet-dev input.ymd
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
The `--dev` flag disables minification and is useful for debugging. Both compilation modes write to stdout.
|
|
@@ -651,9 +651,9 @@ The delimiter passed to `capture_until` is consumed from `remaining` but is **no
|
|
|
651
651
|
You can turn a `.ymd` file into a desktop application:
|
|
652
652
|
|
|
653
653
|
```bash
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
654
|
+
nattoppet-native init # create native/ Rust scaffold
|
|
655
|
+
nattoppet-native bundle file.ymd # compile HTML into native/
|
|
656
|
+
nattoppet-native build file.ymd # compile HTML and run cargo build --release
|
|
657
657
|
```
|
|
658
658
|
|
|
659
659
|
The result is a single native binary (Linux/macOS/Windows) that embeds the HTML and renders it with a web-view.
|
|
@@ -661,7 +661,7 @@ The result is a single native binary (Linux/macOS/Windows) that embeds the HTML
|
|
|
661
661
|
### Project scaffolding
|
|
662
662
|
|
|
663
663
|
```bash
|
|
664
|
-
|
|
664
|
+
nattoppet-init form
|
|
665
665
|
```
|
|
666
666
|
|
|
667
667
|
This generates a starter `main.ymd`, `main.js`, and optionally Rust/WASM boilerplate plus a GitHub Pages deployment workflow.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shiwei Zhang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// nattoppet-dev.ts
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as http from "node:http";
|
|
8
|
+
import * as cp from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
var file = process.argv[2];
|
|
11
|
+
if (process.argv.length < 3 || file == "--help") {
|
|
12
|
+
console.log("Usage: nattoppet-dev [file] [hook cmd]...");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
var runtimeExt = path.extname(fileURLToPath(import.meta.url)) === ".js" ? ".js" : ".ts";
|
|
16
|
+
var nattoppetPath = fileURLToPath(new URL(`nattoppet${runtimeExt}`, import.meta.url));
|
|
17
|
+
var mimeTypes = {
|
|
18
|
+
".html": "text/html",
|
|
19
|
+
".css": "text/css",
|
|
20
|
+
".js": "text/javascript",
|
|
21
|
+
".wasm": "application/wasm",
|
|
22
|
+
".json": "application/json",
|
|
23
|
+
".png": "image/png",
|
|
24
|
+
".svg": "image/svg+xml"
|
|
25
|
+
};
|
|
26
|
+
http.createServer(async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(req.url, "http://localhost");
|
|
29
|
+
if (url.pathname !== "/") {
|
|
30
|
+
const filePath = path.join(process.cwd(), decodeURIComponent(url.pathname));
|
|
31
|
+
await fs.promises.access(filePath);
|
|
32
|
+
const ext = path.extname(filePath);
|
|
33
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
34
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
35
|
+
fs.createReadStream(filePath).pipe(res);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (process.argv.length > 3) {
|
|
39
|
+
const exitCode = await new Promise((resolve) => {
|
|
40
|
+
const hookProc = cp.spawn(process.argv[3], process.argv.slice(4), {
|
|
41
|
+
stdio: "inherit"
|
|
42
|
+
});
|
|
43
|
+
hookProc.on("close", resolve);
|
|
44
|
+
});
|
|
45
|
+
if (exitCode !== 0) {
|
|
46
|
+
console.warn("!!! hook command exit with non-0 status");
|
|
47
|
+
res.writeHead(500);
|
|
48
|
+
res.end("Hook command failed");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
53
|
+
const proc = cp.spawn(process.execPath, [nattoppetPath, file, "--dev"]);
|
|
54
|
+
let stdout2 = "";
|
|
55
|
+
let stderr2 = "";
|
|
56
|
+
proc.stdout.on("data", (d) => stdout2 += d);
|
|
57
|
+
proc.stderr.on("data", (d) => stderr2 += d);
|
|
58
|
+
proc.on("close", (code) => {
|
|
59
|
+
if (code !== 0) reject(new Error(`compilation failed with code ${code}`));
|
|
60
|
+
else resolve({ stdout: stdout2, stderr: stderr2 });
|
|
61
|
+
});
|
|
62
|
+
proc.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
if (stderr)
|
|
65
|
+
console.error(stderr);
|
|
66
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
67
|
+
res.end(stdout);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (e instanceof Error && "code" in e && e.code !== "ENOENT") {
|
|
70
|
+
console.error(e);
|
|
71
|
+
}
|
|
72
|
+
res.writeHead(404);
|
|
73
|
+
res.end("Not found");
|
|
74
|
+
}
|
|
75
|
+
}).listen(3939, () => {
|
|
76
|
+
console.log("Server running at http://127.0.0.1:3939");
|
|
77
|
+
});
|
|
78
|
+
var platform = process.platform;
|
|
79
|
+
var browser = process.env.BROWSER ?? (platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open");
|
|
80
|
+
var browserArgs = browser.includes("chrom") ? [browser, "--app=http://127.0.0.1:3939"] : [browser, "http://127.0.0.1:3939"];
|
|
81
|
+
cp.spawn(browserArgs[0], browserArgs.slice(1), {
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
detached: true
|
|
84
|
+
}).unref();
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// nattoppet-init.ts
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
var type = process.argv[2];
|
|
8
|
+
var avaliable_templates = ["form"];
|
|
7
9
|
if (process.argv.length < 3 || !avaliable_templates.includes(type)) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
console.log("Usage: nattoppet-init form");
|
|
11
|
+
process.exit(0);
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
fs.writeFileSync("main.ymd", `\
|
|
13
|
-
[mixin] ${type}
|
|
13
|
+
fs.writeFileSync("main.ymd", `[mixin] ${type}
|
|
14
14
|
|
|
15
15
|
[title]: Page Title
|
|
16
16
|
|
|
@@ -23,22 +23,23 @@ fs.writeFileSync("main.ymd", `\
|
|
|
23
23
|
[checkbox].female Female
|
|
24
24
|
|
|
25
25
|
[require](main.js)
|
|
26
|
-
`)
|
|
27
|
-
|
|
28
|
-
fs.writeFileSync("main.js", `\
|
|
29
|
-
async function run(args) {
|
|
26
|
+
`);
|
|
27
|
+
fs.writeFileSync("main.js", `async function run(args) {
|
|
30
28
|
return JSON.stringify(args)
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
if (typeof Deno != 'undefined')
|
|
34
32
|
run({}).then(console.log)
|
|
35
|
-
`)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
`);
|
|
34
|
+
var rl = readline.createInterface({
|
|
35
|
+
input: process.stdin,
|
|
36
|
+
output: process.stdout
|
|
37
|
+
});
|
|
38
|
+
var question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
39
|
+
var gen_wasm = await question("Generate wasm? (give lib name or empty to cancel): ");
|
|
40
|
+
rl.close();
|
|
39
41
|
if (gen_wasm) {
|
|
40
|
-
|
|
41
|
-
#[no_mangle]
|
|
42
|
+
fs.writeFileSync(`${gen_wasm}.rs`, `#[no_mangle]
|
|
42
43
|
pub unsafe extern fn sum(data: *const u32, len: usize) -> u32 {
|
|
43
44
|
let mut sum = 0;
|
|
44
45
|
for i in 0..len {
|
|
@@ -58,10 +59,8 @@ pub unsafe extern "C" fn free_memory(ptr: *mut u8, byte_size: usize, alignment:
|
|
|
58
59
|
let layout = std::alloc::Layout::from_size_align_unchecked(byte_size, alignment);
|
|
59
60
|
std::alloc::dealloc(ptr, layout)
|
|
60
61
|
}
|
|
61
|
-
`)
|
|
62
|
-
|
|
63
|
-
fs.writeFileSync("Cargo.toml", `\
|
|
64
|
-
[package]
|
|
62
|
+
`);
|
|
63
|
+
fs.writeFileSync("Cargo.toml", `[package]
|
|
65
64
|
name = "${gen_wasm}"
|
|
66
65
|
version = "0.1.0"
|
|
67
66
|
authors = ["Shiwei Zhang <ylxdzsw@gmail.com>"]
|
|
@@ -78,9 +77,8 @@ strip = true
|
|
|
78
77
|
|
|
79
78
|
[dependencies]
|
|
80
79
|
absurd = { git = "https://github.com/ylxdzsw/absurd" }
|
|
81
|
-
`)
|
|
82
|
-
|
|
83
|
-
fs.appendFileSync("main.js", `
|
|
80
|
+
`);
|
|
81
|
+
fs.appendFileSync("main.js", `
|
|
84
82
|
|
|
85
83
|
async function call_wasm() {
|
|
86
84
|
await window.wasm_ready
|
|
@@ -91,21 +89,21 @@ async function call_wasm() {
|
|
|
91
89
|
${gen_wasm}.free_memory(buffer_ptr, 4 * 8, 4)
|
|
92
90
|
console.log(sum)
|
|
93
91
|
}
|
|
94
|
-
`)
|
|
95
|
-
|
|
96
|
-
fs.appendFileSync("main.ymd", `
|
|
92
|
+
`);
|
|
93
|
+
fs.appendFileSync("main.ymd", `
|
|
97
94
|
[require](target/wasm32-unknown-unknown/release/${gen_wasm}.wasm)
|
|
98
|
-
`)
|
|
95
|
+
`);
|
|
99
96
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
var rl2 = readline.createInterface({
|
|
98
|
+
input: process.stdin,
|
|
99
|
+
output: process.stdout
|
|
100
|
+
});
|
|
101
|
+
var gen_workflow = await new Promise((resolve) => rl2.question("Generate github workflow? (y/n): ", resolve));
|
|
102
|
+
rl2.close();
|
|
104
103
|
if (gen_workflow == "y") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
name: Build and deploy to pages
|
|
104
|
+
fs.mkdirSync(".github", { recursive: true });
|
|
105
|
+
fs.mkdirSync(".github/workflows", { recursive: true });
|
|
106
|
+
fs.writeFileSync(".github/workflows/pages.yml", `name: Build and deploy to pages
|
|
109
107
|
on: [push]
|
|
110
108
|
|
|
111
109
|
jobs:
|
|
@@ -115,10 +113,11 @@ jobs:
|
|
|
115
113
|
- name: Checkout
|
|
116
114
|
uses: actions/checkout@v4
|
|
117
115
|
|
|
118
|
-
- name: Install
|
|
119
|
-
uses:
|
|
116
|
+
- name: Install Node.js
|
|
117
|
+
uses: actions/setup-node@v4
|
|
120
118
|
with:
|
|
121
|
-
|
|
119
|
+
node-version: 22
|
|
120
|
+
|
|
122
121
|
${gen_wasm ? `
|
|
123
122
|
- name: Install Rust
|
|
124
123
|
uses: dtolnay/rust-toolchain@nightly
|
|
@@ -127,16 +126,15 @@ ${gen_wasm ? `
|
|
|
127
126
|
|
|
128
127
|
- name: Compile WASM
|
|
129
128
|
run: cargo build --release --target wasm32-unknown-unknown
|
|
130
|
-
` :
|
|
129
|
+
` : ""}
|
|
131
130
|
- name: Build HTML
|
|
132
|
-
run:
|
|
131
|
+
run: npx --yes nattoppet main.ymd > index.html
|
|
133
132
|
|
|
134
133
|
- name: Deploy
|
|
135
134
|
uses: JamesIves/github-pages-deploy-action@v4
|
|
136
135
|
with:
|
|
137
136
|
branch: gh-pages
|
|
138
137
|
folder: "."
|
|
139
|
-
`)
|
|
138
|
+
`);
|
|
140
139
|
}
|
|
141
|
-
|
|
142
|
-
console.info("Finished")
|
|
140
|
+
console.info("Finished");
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// nattoppet-native.ts
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as cp from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
var main_source = `#![windows_subsystem = "windows"]
|
|
10
|
+
|
|
11
|
+
use web_view::*;
|
|
12
|
+
|
|
13
|
+
fn main() {
|
|
14
|
+
static HTML_CONTENT: &str = include_str!("../target/bundle.html");
|
|
15
|
+
|
|
16
|
+
web_view::builder()
|
|
17
|
+
.title("Nattoppet Native")
|
|
18
|
+
.content(Content::Html(HTML_CONTENT))
|
|
19
|
+
.size(600, 480)
|
|
20
|
+
.resizable(false)
|
|
21
|
+
.debug(cfg!(debug_assertions))
|
|
22
|
+
.user_data(())
|
|
23
|
+
.invoke_handler(handler)
|
|
24
|
+
.run()
|
|
25
|
+
.unwrap();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn handler(webview: &mut WebView<()>, arg: &str) -> WVResult {
|
|
29
|
+
Ok(())
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
var build_source = `#[cfg(windows)]
|
|
33
|
+
extern crate winres;
|
|
34
|
+
|
|
35
|
+
#[cfg(windows)]
|
|
36
|
+
fn main() {
|
|
37
|
+
let mut res = winres::WindowsResource::new();
|
|
38
|
+
res.set_icon("../icon.ico");
|
|
39
|
+
res.compile().unwrap();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(not(windows))]
|
|
43
|
+
fn main() {}
|
|
44
|
+
`;
|
|
45
|
+
var cargo_source = `[package]
|
|
46
|
+
name = "nattoppet_native"
|
|
47
|
+
version = "0.1.0"
|
|
48
|
+
edition = "2021"
|
|
49
|
+
|
|
50
|
+
[dependencies]
|
|
51
|
+
web-view = { version = "0.7", features = ["edge"] }
|
|
52
|
+
|
|
53
|
+
[target.'cfg(windows)'.build-dependencies]
|
|
54
|
+
winres = "0.1"
|
|
55
|
+
|
|
56
|
+
[package.metadata.winres]
|
|
57
|
+
ProductName = "A nattoppet native app"
|
|
58
|
+
`;
|
|
59
|
+
var init = () => {
|
|
60
|
+
fs.mkdirSync("native", { recursive: true });
|
|
61
|
+
fs.mkdirSync("native/src", { recursive: true });
|
|
62
|
+
fs.mkdirSync("native/target", { recursive: true });
|
|
63
|
+
fs.writeFileSync("native/src/main.rs", main_source);
|
|
64
|
+
fs.writeFileSync("native/build.rs", build_source);
|
|
65
|
+
fs.writeFileSync("native/Cargo.toml", cargo_source);
|
|
66
|
+
};
|
|
67
|
+
var spawn2 = (cmd, args, opts) => {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const proc = cp.spawn(cmd, args, opts);
|
|
70
|
+
let stdout = "";
|
|
71
|
+
let stderr = "";
|
|
72
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
73
|
+
proc.stderr.on("data", (d) => stderr += d);
|
|
74
|
+
proc.on("close", (code) => {
|
|
75
|
+
if (code !== 0) reject(new Error(stderr));
|
|
76
|
+
else resolve(stdout);
|
|
77
|
+
});
|
|
78
|
+
proc.on("error", reject);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
var bundle = async () => {
|
|
82
|
+
const runtimeExt = path.extname(fileURLToPath(import.meta.url)) === ".js" ? ".js" : ".ts";
|
|
83
|
+
const nattoppetPath = fileURLToPath(new URL(`nattoppet${runtimeExt}`, import.meta.url));
|
|
84
|
+
const stdout = await spawn2(process.execPath, [nattoppetPath, process.argv[3]]);
|
|
85
|
+
fs.writeFileSync("native/target/bundle.html", stdout);
|
|
86
|
+
};
|
|
87
|
+
var build = async () => {
|
|
88
|
+
await bundle();
|
|
89
|
+
await spawn2("cargo", ["build", "--release"], {
|
|
90
|
+
cwd: "native"
|
|
91
|
+
});
|
|
92
|
+
console.log("building finished. Check native/target/release/");
|
|
93
|
+
};
|
|
94
|
+
var help = () => {
|
|
95
|
+
console.log("Usage: nattoppet-native [init|bundle|build] <file.ymd>");
|
|
96
|
+
};
|
|
97
|
+
switch (process.argv[2]) {
|
|
98
|
+
case "init":
|
|
99
|
+
init();
|
|
100
|
+
break;
|
|
101
|
+
case "bundle":
|
|
102
|
+
bundle().catch((e) => {
|
|
103
|
+
console.error(e.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
case "build":
|
|
108
|
+
build().catch((e) => {
|
|
109
|
+
console.error(e.message);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
help();
|
|
115
|
+
break;
|
|
116
|
+
}
|