diff options
-rwxr-xr-x | amaryllis.cgi | 210 | ||||
-rw-r--r-- | language.lua | 171 | ||||
-rw-r--r-- | marigold.lua | 150 |
3 files changed, 531 insertions, 0 deletions
diff --git a/amaryllis.cgi b/amaryllis.cgi new file mode 100755 index 0000000..83b6e4f --- /dev/null +++ b/amaryllis.cgi @@ -0,0 +1,210 @@ +#!/usr/bin/lua5.1 + +local marigold = require 'marigold' +local lang = require 'language' + +local h = marigold.h +local metavars = marigold.get_metavars() +local settings = marigold.decode_query(metavars.query_string or '') + +settings.seed = tonumber(settings.seed) or os.time() +math.randomseed(settings.seed) + +settings.phonemes = settings.phonemes or [[ +C=mnbdTLshzwrly +V=aeiou +T=1324 +]] + +settings.syllables = settings.syllables or [[ +CVT +CVTl +CVTn +]] + +settings.orthography = settings.orthography or [[ +T=th +L=lh +z=zh +(%V)1=%1́ +(%V)2=%1%1́ +(%V)3=%1 +(%V)4=%1́%1 +]] + +settings.phonemes_decay = settings.phonemes_decay or 'Q' +settings.syllables_decay = settings.syllables_decay or 'Q' + +settings.len_min = settings.len_min or "1" +settings.len_max = settings.len_max or "3" +settings.count = settings.count or "20" + + +local distributions = { + U=lang.uniform, + Q=lang.quadratic, +} + +local p_dist = distributions[settings.phonemes_decay] +local s_dist = distributions[settings.syllables_decay] + + +local l = lang.Language( + settings.phonemes, + settings.syllables, + settings.orthography, + tonumber(settings.len_min), + tonumber(settings.len_max), + p_dist, + s_dist +) + + +local syllable_html = {} +for i=1,tonumber(settings.count) do + table.insert( + syllable_html, + h('li', l:romanize(l:word())) + ) +end +syllable_html = h('ol', syllable_html) + + +function radiobutton(label, name, value, checked) + local radio = h('input', { + type="radio", + name=name, + id = name .. value, + value = value, + checked = (checked and "true") or nil + }) + local lbl = {} + lbl['for'] = name .. value + local label = h('label', label, lbl) + + return h('div', { radio, label }) +end + + +function textarea(name, text, decay) + local children = { class="noborder", + h('legend', name), + h('textarea', text, { name=name, rows="6", spellcheck="false" }), + } + + if decay then table.insert(children, h('fieldset', { + h('legend', name .. ' decay'), + radiobutton("uniform", name .. '_decay', 'U', settings[name .. '_decay'] == 'U'), + radiobutton("quadratic", name .. '_decay', 'Q', settings[name .. '_decay'] == 'Q'), + }) + ) end + + return h('fieldset', children) +end + + +local form = h('form', { + h('label', 'seed', { + h('input', { name="seed", type="number", value=tostring(settings.seed) }), + }), + + h('input', { value="update", type="Submit" } ), + + h('br'), + h('div', { class="form-flex", + textarea('phonemes', settings.phonemes, true), + textarea('syllables', settings.syllables, true), + textarea('orthography', settings.orthography, false), + }), + h('br'), + + h('label', 'min syllables', { + h('input', { name="len_min", type="number", min="1", value=settings.len_min }), + }), + h('br'), + h('label', 'max syllables', { + h('input', { name="len_max", type="number", min="1", value=settings.len_max }), + }), + h('br'), + h('label', '# to generate', { + h('input', { name="count", type="number", min="1", value=settings.count }), + }), + + h('input', { value="update", type="Submit" } ), +}) + + +local body = h('body', { h('div', { id="content", + h('a', '<- projects', { href='https://sanine.net/projects/' }), + h('br'), h('br'), + form, + h('hr'), + syllable_html, + h('a', 'hello there c:', { href="#" }), +})}) + + +local head = h('head', { + h('meta', { charset="utf-8" }), + h('meta', { name="viewport", content="width=device-width, initial-scale=1" }), + h('title', 'amaryllis | sanine.net'), + h('style', [[ + + :root { + --light: #eee; + --dark: #1c1c1c; + --highlight: #f5ae2e; + } + + body { + color: var(--light); + background: var(--dark); + font: 1.3em monospace; + text-size-adjust: auto; + } + + #content { + max-width: 40em; + margin: auto; + } + + .noborder { + border: none; + padding: 0px; + } + + a { + color: var(--highlight); + } + + a:hover { + color: var(--dark); + background: var(--highlight); + text-decoration: none; + } + + .form-flex { + display: flex; + flex-wrap: wrap; + wrap: 100%; + } + + textarea { + color: var(--light); + background: #191919; + border: 1px solid var(--highlight); + } + + input { + font-family: monospace; + color: var(--highlight); + background: var(--dark); + border: 1px solid var(--highlight); + } + ]] + ), +}) + + +print('Content-type: text/html\n') +print(marigold.html(h('html', { head, body }))) diff --git a/language.lua b/language.lua new file mode 100644 index 0000000..cf6725f --- /dev/null +++ b/language.lua @@ -0,0 +1,171 @@ +local language = {} +setmetatable(language, {__index=_G}) +setfenv(1, language) + + +-- local utility functions + +-- convert string to array of characters +local function string_to_array(str) + local arr = {} + for i=1,#str do + table.insert(arr, string.sub(str, i, i)) + end + return arr +end + + +-- iterate over lines in string +local function lines(str) + local clean = function(s) return string.gsub(s, "\n", "") end + return coroutine.wrap(function() + for line in string.gmatch(str, "[^\n]*\n") do + coroutine.yield(clean(line)) + end + local lastline = string.match(str, "\n?[^\n]+$") + if lastline then + coroutine.yield(clean(lastline)) + end + end) +end + + +-- parse phonemes from string +local function parse_phonemes(str) + local phonemes = {} + for line in lines(str) do + if string.match(line, "^%s*#") then + -- this is a comment, ignore + elseif string.match(line, "^%s*$") then + -- ignore blank lines + else + class, phones = string.match(line, "(.)=(.*)") + -- remove whitespace from phones + phones = string.gsub(phones, "%s", "") + phonemes[class] = string_to_array(phones) + end + end + return phonemes +end + + +-- parse syllables from string +local function parse_syllables(str) + local syllables = {} + for line in lines(str) do + line = string.gsub(line, "%s", "") + table.insert(syllables, string_to_array(line)) + end + return syllables +end + + +-- parse orthography patterns from string +local function parse_orthography(str, phonemes) + local rules = {} + for line in lines(str) do + if string.match(str, '^%s*$') then + -- ignore blank lines + else + -- trim whitespace + line = string.gsub(line, "%s*$", "") + local pattern, replace = string.match(line, "(.*)=(.*)") + pattern = string.gsub(pattern, "%%(.)", function(class) + if phonemes[class] == nil then + return '%' .. class + end + return '[' .. table.concat(phonemes[class], '') .. ']' + end) + table.insert(rules, { pattern=pattern, replace=replace }) + end + end + return rules +end + + +-- randomly choose an element from an array with a given distribution +local function random_choice(tbl, distribution) + local x = distribution(math.random()) + local bin = math.floor(x * #tbl) + 1 + return tbl[bin] +end + + +-- probability distributions + +-- uniform (flat) distribution +function uniform(x) + return x +end + + +function quadratic(x) + return x*x +end + + + + + + +Language = {} + +function Language.new(_, phonemes, syllables, orthography, len_min, len_max, p_dist, s_dist, l_dist) + print(len_min, len_max) + local self = {} + setmetatable(self, {__index=Language}) + self.phonemes = parse_phonemes(phonemes) + self.p_dist = p_dist or uniform + self.syllables = parse_syllables(syllables) + self.s_dist = s_dist or uniform + self.orthography = parse_orthography(orthography, self.phonemes) + self.length = {} + for len=len_min,len_max do + table.insert(self.length, len) + end + self.l_dist = l_dist or uniform + return self +end +setmetatable(Language, {__call=Language.new}) + + +function Language.random_phone(self, class) + return random_choice(self.phonemes[class], self.p_dist) +end + + +function Language.syllable(self) + -- pick a random syllable type + local syl = random_choice(self.syllables, self.s_dist) + local result = '' + for _, ch in ipairs(syl) do + if self.phonemes[ch] then + result = result .. self:random_phone(ch) + else + result = result .. ch + end + end + return result +end + + +function Language.word(self) + local len = random_choice(self.length, self.l_dist) + local word = '' + for i=1,len do + word = word .. self:syllable() + end + return word +end + + +function Language.romanize(self, text) + local result = text + for _, rule in ipairs(self.orthography) do + result = string.gsub(result, rule.pattern, rule.replace) + end + return result +end + + +return language diff --git a/marigold.lua b/marigold.lua new file mode 100644 index 0000000..a6223df --- /dev/null +++ b/marigold.lua @@ -0,0 +1,150 @@ +-- ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) +-- +-- marigold-cgi Copyright (c) 2022 Kate Swanson +-- +-- This is anti-capitalist software, released for free use by individuals and +-- organizations that do not operate by capitalist principles. +-- +-- Permission is hereby granted, free of charge, to any person or organization ( +-- the "User") obtaining a copy of this software and associated documentation +-- files (the "Software"), to use, copy, modify, merge, distribute, and/or sell +-- copies of the Software, subject to the following conditions: +-- +-- 1. The above copyright notice and this permission notice shall be included in +-- all copies or modified versions of the Software. +-- +-- 2. The User is one of the following: +-- a. An individual person, laboring for themselves +-- b. A non-profit organization +-- c. An educational institution +-- d. An organization that seeks shared profit for all of its members, and allows +-- non-members to set the cost of their labor +-- +-- 3. If the User is an organization with owners, then all owners are workers and +-- all workers are owners with equal equity and/or equal vote. +-- +-- 4. If the User is an organization, then the User is not law enforcement or +-- military, or working for or under either. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY +-- KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE +-- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +-- CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +local marigold = {} + +marigold.get_metavars = function() + local vars = { + "AUTH_TYPE", "CONTENT_LENGTH", + "CONTENT_TYPE", "GATEWAY_INTERFACE", + "PATH_INFO", "PATH_TRANSLATED", + "QUERY_STRING", "REMOTE_ADDR", + "REMOTE_HOST", "REMOTE_IDENT", + "REMOTE_USER", "REQUEST_METHOD", + "SCRIPT_NAME", "SERVER_NAME", + "SERVER_PORT", "SERVER_PROTOCOL", + "SERVER_SOFTWARE" + } + + local metavars = {} + for _, var in ipairs(vars) do + metavars[string.lower(var)] = os.getenv(var) + end + return metavars +end + + +marigold.h = function(tag_type, content, tbl) + if type(content) == 'table' and tbl == nil then + tbl = content + content = '' + end + + if content == nil then + tbl = {} + content = '' + end + + local tag = {} + tag.tag = tag_type + tag.content = content + tag.attributes = {} + tag.children = {} + + if tbl then + -- add attributes + for k, v in pairs(tbl) do + if type(k) == 'string' then + tag.attributes[k] = v + end + end + -- add children + for _, child in ipairs(tbl) do + table.insert(tag.children, child) + end + end + return tag +end + + +marigold.html = function(tbl, indent_level) + indent_level = indent_level or 0 + local indent = string.rep('\t', indent_level) + + -- generate attribute strings + local attributes = {} + for k, v in pairs(tbl.attributes) do + table.insert(attributes, string.format(' %s="%s"', k, v)) + end + if test then + -- sort alphabetically for well-defined testing + table.sort(attributes) + end + local a = '' + for _, attrib in ipairs(attributes) do + a = a .. attrib + end + + local open = string.format('<%s%s>', tbl.tag, a) + local close = string.format('</%s>', tbl.tag) + + if #tbl.children == 0 then + return string.format('%s%s%s%s', indent, open, tbl.content, close) + end + + local children = '' + for _, child in ipairs(tbl.children) do + children = children .. marigold.html(child, indent_level+1) .. '\n' + end + + return string.format('%s%s%s\n%s%s%s', + indent, open, tbl.content, + children, + indent, close) +end + +marigold.decode_percent = function(str) + return string.gsub(str, "%%(%x%x)", function(digits) + return string.char(tonumber(digits, 16)) + end) +end + +marigold.decode_query = function(str) + local tbl = {} + local cleanString = function(str) + return marigold.decode_percent( + string.gsub(str, "+", " ") + ) + end + + for k, v in string.gmatch(str, "([^=]-)=([^&]*)&?") do + tbl[cleanString(k)] = cleanString(v) + end + + return tbl +end + +return marigold |