nattoppet 2.1.1 → 4.0.1
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 +732 -0
- package/common.ymd +33 -13
- package/compiler.ts +169 -0
- package/form.css +87 -0
- package/form.js +64 -0
- package/form.ymd +36 -0
- package/katex.css +1 -0
- package/katex.ymd +13 -0
- package/koa.css +157 -0
- package/{koa/after.ymd → koa.ymd} +18 -1
- package/nattoppet-dev.ts +94 -0
- package/nattoppet-init.ts +142 -0
- package/nattoppet-native.ts +116 -0
- package/nattoppet.ts +35 -0
- package/package.json +54 -14
- package/ppt.css +304 -0
- package/ppt.js +83 -0
- package/{ppt/after.ymd → ppt.ymd} +17 -1
- package/readme.md +28 -15
- package/{stdlib.js → stdlib.ts} +48 -36
- package/tml.css +125 -0
- package/tml.js +3 -0
- package/{tml/after.ymd → tml.ymd} +19 -1
- package/vue.css +91 -0
- package/vue.ymd +23 -0
- package/.editorconfig +0 -15
- package/assets/katex.css +0 -1
- package/assets/manuscript.cls +0 -30
- package/compiler.js +0 -120
- package/koa/before.ymd +0 -12
- package/koa/koa.less +0 -179
- package/native/Cargo.toml +0 -13
- package/native/build.rs +0 -12
- package/native/src/main.rs +0 -22
- package/nattoppet-dev.js +0 -58
- package/nattoppet-native.js +0 -49
- package/nattoppet.js +0 -26
- package/ppt/before.ymd +0 -8
- package/ppt/ppt.coffee +0 -61
- package/ppt/ppt.less +0 -222
- package/tml/before.ymd +0 -10
- package/tml/tml.coffee +0 -1
- package/tml/tml.less +0 -145
- package/vue/after.ymd +0 -9
- package/vue/before.ymd +0 -9
- package/vue/vue.less +0 -101
package/common.ymd
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
[require]=
|
|
2
|
-
const {args: [file]} = std_call()
|
|
2
|
+
const {opts: [id], args: [file]} = std_call()
|
|
3
3
|
const content = read(file)
|
|
4
4
|
switch (extname(file)) {
|
|
5
5
|
case 'less':
|
|
6
|
-
`<style>${render_less(content)}</style>`; break
|
|
6
|
+
`<style ${id ? `id="${id}"` : ''}>${render_less(content)}</style>`; break
|
|
7
7
|
case 'css':
|
|
8
|
-
`<style>${content}</style>`; break
|
|
8
|
+
`<style ${id ? `id="${id}"` : ''}>${content}</style>`; break
|
|
9
9
|
case 'coffee':
|
|
10
|
-
`<script>${render_coffee(content, { bare: true })}</script>`; break
|
|
10
|
+
`<script ${id ? `id="${id}"` : ''}>${render_coffee(content, { bare: true })}</script>`; break
|
|
11
11
|
case 'js':
|
|
12
|
-
`<script>${content}</script>`; break
|
|
12
|
+
`<script ${id ? `id="${id}"` : ''}>${content}</script>`; break
|
|
13
13
|
case 'md':
|
|
14
14
|
render_markdown(content); break
|
|
15
|
+
case 'wasm':
|
|
16
|
+
`<script ${id ? `id="${id}"` : ''}>
|
|
17
|
+
const ready = (async () => {
|
|
18
|
+
const raw = await fetch("data:application/octet-stream;base64,${read(file, 'compressed-base64')}")
|
|
19
|
+
const blob = await raw.blob()
|
|
20
|
+
const decompressed = await blob.stream().pipeThrough(new DecompressionStream("deflate-raw"))
|
|
21
|
+
const response = new Response(decompressed, { headers: { 'Content-Type': 'application/wasm' } })
|
|
22
|
+
const x = await WebAssembly.instantiateStreaming(response, {})
|
|
23
|
+
window.${file.match(/([^\/]+).wasm$/)[1]} = x.instance.exports
|
|
24
|
+
})()
|
|
25
|
+
window.wasm_ready = window.wasm_ready ? Promise.all([window.wasm_ready, ready]) : ready
|
|
26
|
+
</script>`; break
|
|
27
|
+
case 'json':
|
|
28
|
+
`<script ${id ? `id="${id}"` : ''}>
|
|
29
|
+
const ready = (async () => {
|
|
30
|
+
const raw = await fetch("data:application/octet-stream;base64,${read(file, 'compressed-base64')}")
|
|
31
|
+
const blob = await raw.blob()
|
|
32
|
+
const decompressed = await blob.stream().pipeThrough(new DecompressionStream("deflate-raw"))
|
|
33
|
+
const response = new Response(decompressed, { headers: { 'Content-Type': 'application/json' } })
|
|
34
|
+
window.${file.match(/([^\/]+).json$/)[1]} = await response.json()
|
|
35
|
+
})()
|
|
36
|
+
window.json_ready = window.json_ready ? Promise.all([window.json_ready, ready]) : ready
|
|
37
|
+
</script>`; break
|
|
15
38
|
default:
|
|
16
39
|
content; break
|
|
17
40
|
}
|
|
@@ -57,14 +80,6 @@ const content = text || capture_until('\n').trim() || href
|
|
|
57
80
|
const {args: [text]} = std_call()
|
|
58
81
|
;`<sup>${interpret(text)}</sup>`
|
|
59
82
|
|
|
60
|
-
[$]=
|
|
61
|
-
const tex = capture_until('$')
|
|
62
|
-
render_katex(tex, false)
|
|
63
|
-
|
|
64
|
-
[$$]=
|
|
65
|
-
const {block} = std_call(true)
|
|
66
|
-
render_katex(block, true)
|
|
67
|
-
|
|
68
83
|
[eval]=
|
|
69
84
|
const {block} = std_call(true)
|
|
70
85
|
eval(block)
|
|
@@ -72,3 +87,8 @@ eval(block)
|
|
|
72
87
|
[eval*]=
|
|
73
88
|
const {block} = std_call(true)
|
|
74
89
|
eval(block); ''
|
|
90
|
+
|
|
91
|
+
[cn]=
|
|
92
|
+
interpret(std_call().block)
|
|
93
|
+
.replace(/([\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\ud840-\ud868][\udc00-\udfff]|\ud869[\udc00-\uded6\udf00-\udfff]|[\ud86a-\ud86c][\udc00-\udfff]|\ud86d[\udc00-\udf34\udf40-\udfff]|\ud86e[\udc00-\udc1d])\r?\n(?=\S)/g, '$1')
|
|
94
|
+
.replace(/(?<=\S)\r?\n([\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\ud840-\ud868][\udc00-\udfff]|\ud869[\udc00-\uded6\udf00-\udfff]|[\ud86a-\ud86c][\udc00-\udfff]|\ud86d[\udc00-\udf34\udf40-\udfff]|\ud86e[\udc00-\udc1d])/g, '$1')
|
package/compiler.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import vm from "node:vm"
|
|
2
|
+
import { extname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import * as fs from "node:fs"
|
|
5
|
+
|
|
6
|
+
const pattern = /^(?:\[(.+?)\]([:=])|\[(mixin|#slot)\] ?(.*?)\n)/m
|
|
7
|
+
|
|
8
|
+
const fetch_text_file = async (path: string) => {
|
|
9
|
+
const filePath = new URL(path, import.meta.url)
|
|
10
|
+
return fs.readFileSync(fileURLToPath(filePath), 'utf-8')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const file_exists = (path: string) => {
|
|
14
|
+
try {
|
|
15
|
+
const filePath = new URL(path, import.meta.url)
|
|
16
|
+
fs.accessSync(fileURLToPath(filePath))
|
|
17
|
+
return true
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const resolve_mixin_path = (path: string): string => {
|
|
24
|
+
if (extname(path)) return path
|
|
25
|
+
// Compatibility hack: bare name only resolves to .ymd if exact file doesn't exist
|
|
26
|
+
if (file_exists(path)) return path
|
|
27
|
+
const ymdPath = path + ".ymd"
|
|
28
|
+
if (file_exists(ymdPath)) return ymdPath
|
|
29
|
+
throw `mixin not found: ${path}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// tokenize also process mixins
|
|
33
|
+
// mixins are special direvatives
|
|
34
|
+
// by default, they are relative to the root directory of nattoppet, unless they are prefixed with "./"
|
|
35
|
+
export const tokenize = async (str: string) => {
|
|
36
|
+
const tokens: any[] = []
|
|
37
|
+
|
|
38
|
+
while (true) {
|
|
39
|
+
const m = str.match(pattern)
|
|
40
|
+
if (!m || m.index == null) {
|
|
41
|
+
if (str) tokens.push({ type: "code", content: str })
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (m.index && m.index > 0) {
|
|
46
|
+
tokens.push({ type: "code", content: str.substring(0, m.index) })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (m[3]) {
|
|
50
|
+
if (m[3] === 'mixin') {
|
|
51
|
+
tokens.push({ type: 'mixin', path: m[4] })
|
|
52
|
+
} else if (m[3] === '#slot') {
|
|
53
|
+
tokens.push({ type: 'slot' })
|
|
54
|
+
}
|
|
55
|
+
str = str.substring(m.index + m[0].length)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const name = m[1]
|
|
60
|
+
const type = m[2] == ':' ? 'ref' : 'fn'
|
|
61
|
+
str = str.substring(m.index + m[0].length)
|
|
62
|
+
|
|
63
|
+
const p1 = str.indexOf('\n\n')
|
|
64
|
+
const p2 = str.search(pattern)
|
|
65
|
+
const p = p1 >= 0 && p2 >= 0 ? Math.min(p1+2, p2) :
|
|
66
|
+
p1 < 0 && p2 < 0 ? str.length : Math.max(p1+2, p2)
|
|
67
|
+
|
|
68
|
+
const content = str.substring(0, p).trim()
|
|
69
|
+
str = str.substring(p)
|
|
70
|
+
|
|
71
|
+
tokens.push({ type, name, content })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < tokens.length; i++) if (tokens[i].type == 'mixin') {
|
|
75
|
+
const resolvedPath = resolve_mixin_path(tokens[i].path)
|
|
76
|
+
const ext = extname(resolvedPath)
|
|
77
|
+
|
|
78
|
+
if (ext === ".ymd") {
|
|
79
|
+
const text = await fetch_text_file(resolvedPath)
|
|
80
|
+
const inlineTokens = await tokenize(text)
|
|
81
|
+
const slotIndices = inlineTokens
|
|
82
|
+
.map((t: any, idx: number) => t.type === 'slot' ? idx : -1)
|
|
83
|
+
.filter((idx: number) => idx >= 0)
|
|
84
|
+
|
|
85
|
+
if (slotIndices.length > 1) {
|
|
86
|
+
throw `multiple [#slot] in mixin ${resolvedPath}`
|
|
87
|
+
} else if (slotIndices.length === 1) {
|
|
88
|
+
const slotIdx = slotIndices[0]
|
|
89
|
+
const head = inlineTokens.slice(0, slotIdx)
|
|
90
|
+
const tail = inlineTokens.slice(slotIdx + 1)
|
|
91
|
+
tokens.splice(i, 1, ...head)
|
|
92
|
+
tokens.push(...tail)
|
|
93
|
+
} else {
|
|
94
|
+
tokens.splice(i, 1, ...inlineTokens)
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
tokens[i] = { type: "raw", content: await fetch_text_file(resolvedPath) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
i -= 1 // revisit i since we replaced it
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return tokens
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const _interpret = (str: string, env: any, defs: any[]): any => {
|
|
107
|
+
const p1 = str.search(/^ /m)
|
|
108
|
+
const p2 = str.search(/\[(.+?)\]/)
|
|
109
|
+
if (p1 < 0 && p2 < 0) return str
|
|
110
|
+
|
|
111
|
+
if (p2 < 0 || (p1 >= 0 && p1 < p2)) {
|
|
112
|
+
const head = str.substring(0, p1)
|
|
113
|
+
str = str.substring(p1+2)
|
|
114
|
+
let p = str.indexOf('\n\n')
|
|
115
|
+
if (p < 0) p = str.length
|
|
116
|
+
return head + '<p>' + _interpret(str.substring(0, p), env, defs) + '</p>' + _interpret(str.substring(p+2), env, defs)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const name = str.match(/\[(.+?)\]/)![1]
|
|
120
|
+
const i = defs.findIndex(x => x.name == name)
|
|
121
|
+
if (i < 0) throw `definition ${name} not found`
|
|
122
|
+
|
|
123
|
+
const def = defs[i]
|
|
124
|
+
switch (def.type) {
|
|
125
|
+
case 'fn':
|
|
126
|
+
env.remaining = str.substring(p2 + name.length + 2)
|
|
127
|
+
env.interpret = (str: string) => { // a "scoped" interpret function capable for interpreting substrings. We preserve the "remaining" property outside.
|
|
128
|
+
const remaining = env.remaining
|
|
129
|
+
const result = _interpret(str, env, defs) // We gaved full defs available on the call site (dynamic scoping) as the text to be interpreted is part of the call site text.
|
|
130
|
+
env.remaining = remaining
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
const result = vm.runInContext('{' + def.content + '}', env)
|
|
134
|
+
return str.substring(0, p2) + result + _interpret(env.remaining, env, defs)
|
|
135
|
+
case 'ref':
|
|
136
|
+
return str.substring(0, p2) +
|
|
137
|
+
_interpret(def.content, env, defs.slice(i+1)) + // Exclude self from scope to prevent infinite recursion, but allow forward references
|
|
138
|
+
_interpret(str.substring(p2 + name.length + 2), env, defs)
|
|
139
|
+
default: throw 'unknown defs type'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const compile = async (str: string, locals: any = {}) => {
|
|
144
|
+
const env = vm.createContext(locals)
|
|
145
|
+
for (const k in env) if (typeof(env[k]) == 'function')
|
|
146
|
+
env[k] = env[k].bind(env)
|
|
147
|
+
|
|
148
|
+
let tokens = await tokenize(str)
|
|
149
|
+
let output = ''
|
|
150
|
+
while (true) {
|
|
151
|
+
const token = tokens.shift()
|
|
152
|
+
if (!token) return output
|
|
153
|
+
switch (token.type) {
|
|
154
|
+
case "code":
|
|
155
|
+
output += _interpret(token.content, env, tokens)
|
|
156
|
+
break
|
|
157
|
+
case "raw":
|
|
158
|
+
output += token.content
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
TODO:
|
|
166
|
+
1. support first-class markdown-like nestable lists
|
|
167
|
+
2. cleanup the spaces, carefully define when to trim
|
|
168
|
+
3. require a newline before indent to open paragraph?
|
|
169
|
+
*/
|
package/form.css
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
html {
|
|
2
|
+
font-family: sans-serif;
|
|
3
|
+
font-size: 16px;
|
|
4
|
+
line-height: 1.5;
|
|
5
|
+
text-align: justify;
|
|
6
|
+
hyphens: auto;
|
|
7
|
+
color: #33333d;
|
|
8
|
+
}
|
|
9
|
+
@media (max-width: 425px) {
|
|
10
|
+
html {
|
|
11
|
+
font-size: 15px;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
body {
|
|
15
|
+
max-width: 625px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
padding: 1rem 1rem;
|
|
18
|
+
}
|
|
19
|
+
@media (max-width: 425px) {
|
|
20
|
+
body {
|
|
21
|
+
padding: 0 0.8rem;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
h3 {
|
|
25
|
+
margin: 2rem 0 1rem;
|
|
26
|
+
position: relative;
|
|
27
|
+
border-bottom: #bbb 1px solid;
|
|
28
|
+
}
|
|
29
|
+
label {
|
|
30
|
+
display: block;
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
font-size: 0.9rem;
|
|
33
|
+
}
|
|
34
|
+
label input:not([type]),
|
|
35
|
+
label input[type="text"],
|
|
36
|
+
label input[type="number"] {
|
|
37
|
+
display: block;
|
|
38
|
+
width: 100%;
|
|
39
|
+
margin: 0.1rem 0 1rem;
|
|
40
|
+
padding: 0.4rem;
|
|
41
|
+
border: 1px solid #bbb;
|
|
42
|
+
border-radius: 3px;
|
|
43
|
+
font-size: 0.9rem;
|
|
44
|
+
box-sizing: border-box;
|
|
45
|
+
}
|
|
46
|
+
label input[type="checkbox"] {
|
|
47
|
+
margin: 0.1rem 0.5rem 1rem 0;
|
|
48
|
+
transform: translateY(0.1rem);
|
|
49
|
+
}
|
|
50
|
+
button {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
appearance: none;
|
|
53
|
+
background-color: transparent;
|
|
54
|
+
border: 1px solid #bbb;
|
|
55
|
+
border-radius: 0.375rem;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
line-height: 100%;
|
|
58
|
+
padding: 0.6rem 0.8rem;
|
|
59
|
+
}
|
|
60
|
+
button:active {
|
|
61
|
+
border-color: #4a4a4a;
|
|
62
|
+
outline: 0;
|
|
63
|
+
}
|
|
64
|
+
button:focus {
|
|
65
|
+
border-color: #485fc7;
|
|
66
|
+
outline: 0;
|
|
67
|
+
}
|
|
68
|
+
button:hover {
|
|
69
|
+
border-color: #b5b5b5;
|
|
70
|
+
}
|
|
71
|
+
button:focus:not(:active) {
|
|
72
|
+
box-shadow: rgba(72, 95, 199, 0.25) 0 0 0 0.125rem;
|
|
73
|
+
}
|
|
74
|
+
pre {
|
|
75
|
+
clear: both;
|
|
76
|
+
margin-top: 1rem;
|
|
77
|
+
font-family: monospace;
|
|
78
|
+
color: #222;
|
|
79
|
+
border: 1px solid #bcd;
|
|
80
|
+
background-color: #ebf1f5;
|
|
81
|
+
padding: 0.8rem;
|
|
82
|
+
overflow-x: auto;
|
|
83
|
+
font-size: 0.8rem;
|
|
84
|
+
}
|
|
85
|
+
.hidden {
|
|
86
|
+
opacity: 0;
|
|
87
|
+
}
|
package/form.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function gather_inputs() {
|
|
2
|
+
const args = Object.create(null)
|
|
3
|
+
|
|
4
|
+
for (const e of document.querySelectorAll('input')) {
|
|
5
|
+
switch (e.type) {
|
|
6
|
+
case 'text':
|
|
7
|
+
args[e.name] = e.value
|
|
8
|
+
break;
|
|
9
|
+
case 'number':
|
|
10
|
+
args[e.name] = parseFloat(e.value)
|
|
11
|
+
break;
|
|
12
|
+
case 'checkbox':
|
|
13
|
+
args[e.name] = e.checked
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return args
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
window.addEventListener('load', async () => {
|
|
22
|
+
document.querySelector('button').addEventListener('click', async () => {
|
|
23
|
+
document.querySelector('pre').classList.remove('hidden')
|
|
24
|
+
document.querySelector('button').disabled = true
|
|
25
|
+
await new Promise(res => setTimeout(res, 0))
|
|
26
|
+
|
|
27
|
+
const args = gather_inputs()
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await run(args)
|
|
31
|
+
if (result)
|
|
32
|
+
document.querySelector('pre').textContent = result
|
|
33
|
+
} catch (e) {
|
|
34
|
+
document.querySelector('pre').textContent = "Error: " + (e.message ?? e)
|
|
35
|
+
throw e
|
|
36
|
+
} finally {
|
|
37
|
+
document.querySelector('button').disabled = false
|
|
38
|
+
await new Promise(res => setTimeout(res, 0))
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
document.body.addEventListener('input', () => {
|
|
43
|
+
const url_params = new URLSearchParams(gather_inputs())
|
|
44
|
+
history.replaceState(null, '', '#' + url_params.toString())
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (location.hash.length > 1) {
|
|
48
|
+
const url_params = new URLSearchParams(location.hash.slice(1))
|
|
49
|
+
for (const [name, value] of url_params.entries()) {
|
|
50
|
+
const e = document.querySelector(`input[name="${name}"]`)
|
|
51
|
+
if (!e) continue
|
|
52
|
+
|
|
53
|
+
switch (e.type) {
|
|
54
|
+
case 'text':
|
|
55
|
+
case 'number':
|
|
56
|
+
e.value = value
|
|
57
|
+
break;
|
|
58
|
+
case 'checkbox':
|
|
59
|
+
e.checked = value == 'true'
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
package/form.ymd
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta name=viewport content="width=device-width, initial-scale=1.0">
|
|
3
|
+
<meta name=author content="ylxdzsw@gmail.com">
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>[title] | ylxdzsw's blog</title>
|
|
6
|
+
|
|
7
|
+
<style>
|
|
8
|
+
[mixin] form.css
|
|
9
|
+
</style>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
[mixin] form.js
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
[#slot]
|
|
16
|
+
|
|
17
|
+
<button>Run</button>
|
|
18
|
+
<pre class="hidden"></pre>
|
|
19
|
+
|
|
20
|
+
[h3]=
|
|
21
|
+
const {block: content} = std_call(true)
|
|
22
|
+
;`<h3>${content}</h3>`
|
|
23
|
+
|
|
24
|
+
[text]=
|
|
25
|
+
const {opts: [name], args: [attr=""], block: desc} = std_call(true)
|
|
26
|
+
;`<label>${interpret(desc).trim()}<input name="${name}" ${attr}></input></label>`
|
|
27
|
+
|
|
28
|
+
[number]=
|
|
29
|
+
const {opts: [name], args: [attr=""], block: desc} = std_call(true)
|
|
30
|
+
;`<label>${interpret(desc).trim()}<input type="number" name="${name}" ${attr}></input></label>`
|
|
31
|
+
|
|
32
|
+
[checkbox]=
|
|
33
|
+
const {opts: [name], args: [attr=""], block: desc} = std_call(true)
|
|
34
|
+
;`<label><input type="checkbox" name="${name}" ${attr}></input>${interpret(desc).trim()}</label>`
|
|
35
|
+
|
|
36
|
+
[mixin] common.ymd
|