1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
#!/usr/bin/env lua5.1
local marigold = require 'marigold-cgi.marigold'
local h = marigold.h
local b64 = require '3rdparty.base64'
local json = require '3rdparty.json'
-- get statistics
local metavars = marigold.get_metavars()
local query = marigold.decode_query(metavars.query_string or '')
local function query_retrieve(key, default)
local key = 'stat_'..key
if query[key] ~= '' then return query[key]
else return default
end
end
local stat = {
name = query_retrieve('name', 'Name'),
alignment = query_retrieve('alignment', 'neutral'),
size = query_retrieve('size', 'medium'),
type = query_retrieve('type', 'creature'),
ac = query_retrieve('ac', '0'),
hp = query_retrieve('hp', '0'),
speed = query_retrieve('speed', '0 ft.'),
str = query_retrieve('str', '10'),
dex = query_retrieve('dex', '10'),
con = query_retrieve('con', '10'),
int = query_retrieve('int', '10'),
wis = query_retrieve('wis', '10'),
cha = query_retrieve('cha', '10'),
saving_throws = (query.stat_saving_throws ~= '' and query.stat_saving_throws) or '',
vulnerabilities = (query.stat_vulnerabilities ~= '' and query.stat_vulnerabilities) or '',
resistances = (query.stat_reistances ~= '' and query.stat_reistances) or '',
immunities = (query.stat_immunities ~= '' and query.stat_immunities) or '',
condition_immunities = (query.stat_condition_immunities ~= '' and query.stat_condition_immunities) or '',
languages = (query.stat_languages ~= '' and query.stat_languages) or '',
cr = (query.stat_cr ~= '' and query.stat_cr) or '',
traits = (query.stat_traits ~= '' and query.stat_traits) or [[@New Trait
Add a named trait with an at sign. All lines following until the next at sign
will be part of the trait description!
@Second Trait
This is a different trait.
]],
actions = (query.stat_actions ~= '' and query.stat_actions) or [[
@New Action
Actions (and reactions and legendary actions) use the same syntax as traits. As well,
in all of them you can add *italics* and **bold** text!
]],
reactions = (query.stat_reactions ~= '' and query.stat_reactions) or '',
legendary_description = (query.stat_legendary_description ~= '' and query.stat_legendary_description) or '',
legendary = (query.stat_legendary ~= '' and query.stat_legendary) or '',
}
-- stats yarrow query conversion
local function clean(str)
local str = string.gsub(str, '^%s+', '')
local str = string.gsub(str, '%s+$', '')
return str
end
local function parse_traits(str)
local tbl = {}
for name, value in string.gmatch(str, '@([^@]-)\n([^@]+)') do
table.insert(tbl, {name=clean(name), value=clean(value)})
end
return tbl
end
local function convert(stats)
local tbl = {}
for k, v in pairs(stats) do
if v ~= '' then
tbl[k] = v
end
end
tbl.traits = parse_traits(stats.traits)
tbl.actions = parse_traits(stats.actions)
tbl.reactions = parse_traits(stats.reactions)
if stats.legendary ~= '' then
tbl.legendary = {}
tbl.legendary.description = stats.legendary_description
tbl.legendary.actions = parse_traits(stats.legendary)
end
return json.encode(tbl)
end
-- form helpers
local function text_input(label, name, value)
local tbl = {}
tbl['for'] = name
return h('div', { class='inputdiv',
h('label', label, tbl),
h('input', { type='text', id=name, name=name, value=value }),
})
end
local function textarea(label, name, value, height)
local tbl = {}
tbl['for'] = name
return h('div', { class='inputdiv',
h('label', label, tbl),
h('textarea', value, { id=name, name=name, style=(height and 'height:'..height) or nil }),
})
end
-- stats form
local form_basic = h('form', { style="width:100%; max-width: 30em; margin: auto; padding: 3em 0;",
text_input('Name', 'stat_name', stat.name),
text_input('Alignment', 'stat_alignment', stat.alignment),
text_input('Size', 'stat_size', stat.size),
text_input('Type', 'stat_type', stat.type),
h('br'),
text_input('Armor Class', 'stat_ac', stat.ac),
text_input('Hit Points', 'stat_hp', stat.hp),
text_input('Speed', 'stat_speed', stat.speed),
h('br'),
text_input('STR', 'stat_str', stat.str),
text_input('DEX', 'stat_dex', stat.dex),
text_input('CON', 'stat_con', stat.dex),
text_input('INT', 'stat_int', stat.int),
text_input('WIS', 'stat_wis', stat.wis),
text_input('CHA', 'stat_cha', stat.cha),
h('br'),
text_input('Saving Throws', 'stat_saving_throws', stat.saving_throws),
text_input('Damage Vulnerabilities', 'stat_vulnerabilities', stat.vulnerabilities),
text_input('Damage Resistances', 'stat_resistances', stat.resistances),
text_input('Damage Immunities', 'stat_immunities', stat.immunities),
text_input('Condition Immunities', 'stat_condition_immunities', stat.condition_immunities),
text_input('Senses', 'stat_senses', stat.senses),
text_input('Languages', 'stat_languages', stat.languages),
text_input('Challenge', 'stat_cr', stat.cr),
h('br'),
textarea('Additional Traits', 'stat_traits', stat.traits, '20em'),
h('br'),
textarea('Actions', 'stat_actions', stat.actions, '20em'),
h('br'),
textarea('Reactions', 'stat_reactions', stat.reactions, '20em'),
h('br'),
textarea('Legendary Action Text', 'stat_legendary_description', stat.legendary_description, '5em'),
textarea('Legendary Actions', 'stat_legendary', stat.legendary, '20em'),
h('br'),
h('div', { style='text-align:center',
h('button', 'Update', { type='submit' }),
})
})
local img = h('div', { style='text-align: center',
h('img', { style="max-width:500px", width='90%', src='yarrow.cgi?statistics='..b64.encode(convert(stat)) }),
h('br'),
h('a', 'image permalink', { id="permalink", href='yarrow.cgi?statistics='..b64.encode(convert(stat)) }),
})
local head = h('head', {
h('meta', { charset='utf-8' }),
h('meta', { name='viewport', content='width=device-width, initial-scale=1' }),
h('title', 'yarrow config'),
h('link', { rel='stylesheet', href='style.css' }),
h('script', string.format([[
window.onload = () => {
const link = document.getElementById('permalink');
const br = document.createElement('br');
const button = document.createElement('button');
button.textContent = 'copy link';
button.addEventListener('click', () => {
navigator.clipboard.writeText('%s');
button.textContent = 'copied!';
setTimeout(() => button.textContent = 'copy link', 1000);
});
link.after(br, button);
};
]], 'https://sanine.net/utils/yarrow/yarrow.cgi?statistics='..b64.encode(convert(stat))), { type='text/javascript' }),
})
local traits = parse_traits(stat.traits)
local traits_elements = {}
for _, t in ipairs(traits) do
table.insert(traits_elements, h('p', string.format('%s -> %s', t.name, t.value)))
end
local body = h('body', {
form_basic,
img,
h('hr'),
h('a', 'source code', { href='https://sanine.net/git/yarrow' }),
})
print('Content-type: text/html\n')
print(marigold.html(h('html', { head, body })))
|