summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xamaryllis.cgi210
-rw-r--r--language.lua171
-rw-r--r--marigold.lua150
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