From 30fb0dc40d595b9e0b7747ad8e5c4e8cf7e3023b Mon Sep 17 00:00:00 2001 From: s-ol Date: Thu, 1 Nov 2018 21:17:55 +1100 Subject: move mmm code from lib to mmm etc --- Tupfile | 10 +- app/init.moon | 158 ------------------------ app/test_component.moon | 212 --------------------------------- app/todo.moon | 35 ------ lib/canvasapp.client.moon | 88 -------------- lib/color.moon | 20 ---- lib/component.client.moon | 158 ------------------------ lib/component.server.moon | 130 -------------------- lib/dom.client.moon | 33 ------ lib/dom.server.moon | 40 ------- lib/duct_tape.server.moon | 130 -------------------- lib/init.client.moon | 49 -------- lib/init.server.moon | 60 ---------- lib/mmmfs/browser.moon | 122 ------------------- lib/mmmfs/conversion.moon | 118 ------------------ lib/mmmfs/fileder.moon | 155 ------------------------ lib/mmmfs/init.moon | 34 ------ lib/ordered.moon | 27 ----- mmm/canvasapp.client.moon | 88 ++++++++++++++ mmm/color.moon | 20 ++++ mmm/component/init.client.moon | 152 ++++++++++++++++++++++++ mmm/component/init.server.moon | 123 +++++++++++++++++++ mmm/dom/init.client.moon | 33 ++++++ mmm/dom/init.server.moon | 40 +++++++ mmm/init.client.moon | 49 ++++++++ mmm/init.server.moon | 46 +++++++ mmm/mmmfs/browser.moon | 122 +++++++++++++++++++ mmm/mmmfs/conversion.moon | 118 ++++++++++++++++++ mmm/mmmfs/fileder.moon | 155 ++++++++++++++++++++++++ mmm/mmmfs/init.moon | 40 +++++++ mmm/ordered.moon | 27 +++++ render.moon | 21 ++-- root/animations/init.moon | 4 +- root/animations/koch.moon | 6 +- root/animations/twisted.moon | 6 +- root/articles/init.moon | 4 +- root/articles/mmmfs/gallery.moon | 6 +- root/articles/mmmfs/init.moon | 8 +- root/articles/realities.moon | 4 +- root/experiments/center_of_mass.moon | 8 +- root/experiments/init.moon | 4 +- root/experiments/tags/init.moon | 6 +- root/init.moon | 7 +- root/meta/init.moon | 27 +++++ root/meta/test_component.moon | 224 +++++++++++++++++++++++++++++++++++ root/meta/todo.moon | 42 +++++++ tup.moon | 13 +- 47 files changed, 1364 insertions(+), 1618 deletions(-) delete mode 100644 app/init.moon delete mode 100644 app/test_component.moon delete mode 100644 app/todo.moon delete mode 100644 lib/canvasapp.client.moon delete mode 100644 lib/color.moon delete mode 100644 lib/component.client.moon delete mode 100644 lib/component.server.moon delete mode 100644 lib/dom.client.moon delete mode 100644 lib/dom.server.moon delete mode 100644 lib/duct_tape.server.moon delete mode 100644 lib/init.client.moon delete mode 100644 lib/init.server.moon delete mode 100644 lib/mmmfs/browser.moon delete mode 100644 lib/mmmfs/conversion.moon delete mode 100644 lib/mmmfs/fileder.moon delete mode 100644 lib/mmmfs/init.moon delete mode 100644 lib/ordered.moon create mode 100644 mmm/canvasapp.client.moon create mode 100644 mmm/color.moon create mode 100644 mmm/component/init.client.moon create mode 100644 mmm/component/init.server.moon create mode 100644 mmm/dom/init.client.moon create mode 100644 mmm/dom/init.server.moon create mode 100644 mmm/init.client.moon create mode 100644 mmm/init.server.moon create mode 100644 mmm/mmmfs/browser.moon create mode 100644 mmm/mmmfs/conversion.moon create mode 100644 mmm/mmmfs/fileder.moon create mode 100644 mmm/mmmfs/init.moon create mode 100644 mmm/ordered.moon create mode 100644 root/meta/init.moon create mode 100644 root/meta/test_component.moon create mode 100644 root/meta/todo.moon diff --git a/Tupfile b/Tupfile index 7490dcf..370dfd9 100644 --- a/Tupfile +++ b/Tupfile @@ -11,14 +11,20 @@ # render stylesheet : scss/main.scss |> !sassc |> dist/main.css -preload lib -preload lib/mmmfs +# preload full mmmfs tree preload root preload root/animations preload root/articles preload root/articles/mmmfs preload root/experiments preload root/experiments/tags +preload root/meta + +# preload all of mmm +preload mmm +preload mmm/dom +preload mmm/mmmfs +preload mmm/component # render static content and compile sources run moon tup.moon diff --git a/app/init.moon b/app/init.moon deleted file mode 100644 index 8bc95e3..0000000 --- a/app/init.moon +++ /dev/null @@ -1,158 +0,0 @@ -require = relative ... - -union = (...) -> with res = {} - c = 1 - for i = 1, select '#', ... - tbl = select i, ... - for val in *tbl - res[c] = val - c += 1 - -map = (f, list) -> [f val for val in *list] - -patch_redirs = -> - redirs = - 'center-of-mass': 'center_of_mass', - 'test-component': 'test_component', - 'play-tags': 'tags', - - { :location } = window - if (not location.search\find '=') and #location.search > 1 - name = location.search\sub 2 - location.href = redirs[name] or name - -client_goto = -> - { :location } = window - if module = location.search\match 'client=([%w_]+)' - document.body.innerHTML = '' - require "app.#{module}" - -articles = { - { - name: 'realities' - desc: 'exploring the nesting relationships of virtual and other realities' - }, - { - name: 'mmmfs' - desc: 'a file and operating system to live in (wip)' - }, -} - -animations = { - { - name: 'twisted' - desc: 'pseudo 3d animation' - }, - { - name: 'koch' - desc: "lil' fractal thing" - }, -} - -experiments = { - { - name: 'center_of_mass' - desc: 'aligning characters by their centers of mass' - }, - { - name: 'tags' - desc: 'organizing files with Functional Tags' - }, -} - -meta = { - { - name: 'todo' - desc: 'Todo MVC with the mmm UI framework' - }, - { - name: 'test_component' - desc: 'test suite for the mmm reactive UI framework' - }, -} - -content = { - { 'articles', articles } - { 'animations', animations } - { 'experiments', experiments } - { 'meta', meta } -} - -index = { - name: 'index' - route: '' - dest: 'index.html' - render: => - import h1, h3, div, b, p, a, br, ul, tt, li, img from require 'lib.dom' - import opairs from require 'lib.ordered' - - moon = '\xe2\x98\xbd' - - -- redirects for old-style URIs - on_client patch_redirs - - iconlink = (href, src, alt, style) -> a { - class: 'iconlink', - :href, - target: '_blank', - img :src, :alt, :style - } - - -- menu - append h1 { - style: { - position: 'relative', - 'border-bottom': '1px solid #000' - }, - 'mmm', - div { - class: 'icons', - iconlink 'https://github.com/s-ol/mmm', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/github.svg', - iconlink 'https://twitter.com/S0lll0s', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/twitter.svg', - iconlink 'https://webring.xxiivv.com/#random', 'https://webring.xxiivv.com/icon.black.svg', 'webring', - { height: '0.9em', 'margin-left': '.04em' } - } - } - - append p { - tt 'mmm' - ' is not the ' - tt 'www' - ', because it runs on ' - a { 'MoonScript', href: 'https://moonscript.org' } - '.' - br! - 'You can find the source code of everything ' - a { 'here', href: 'https://github.com/s-ol/mmm' } - '.' - } - - for { category, routes } in *content - append h3 category, style: { 'margin-bottom': '-.5em' } - append ul for { :name, :desc, :route } in *routes - li (a name, href: route), ': ', desc - - append p { - "made with #{moon} by " - a { 's-ol', href: 'https://twitter.com/S0lll0s' } - } - - on_client client_goto -} - -load = (module) -> require "app.#{module}" - -destify = (route) -> with route - .route or= .name - .dest or= "#{.route}/index.html" - - if .render == 'client' - .render = -> on_client load, .name - .render or= -> require '.' .. .name - -routes = map destify, union articles, animations, experiments, meta, { index } -{ - :routes, - indexed: { r.name, r for r in *routes } - render: (name) -> require ".#{name}" -} diff --git a/app/test_component.moon b/app/test_component.moon deleted file mode 100644 index d3f1489..0000000 --- a/app/test_component.moon +++ /dev/null @@ -1,212 +0,0 @@ -on_client -> - import div, h1, ul, li, pre from require 'lib.dom' - - last = nil - test_group = (name) -> - if last - append div (h1 name), ul last - - last = {} - (name, test) -> - ok, err = pcall test - table.insert last, li if ok - "passed '#{name}'" - else - "failed '#{name}'", pre err - - expect = (expected, note, ...) -> - ok, msg = pcall ... - print ok, msg\find expected - if ok or not msg\find expected - error note - - run_test = test_group 'component.moon' - - local ReactiveVar, ReactiveElement - run_test "exports ReactiveVar, ReactiveElement", -> - import ReactiveVar, ReactiveElement from require 'lib.component' - assert ReactiveVar, "ReactiveVar not exported" - assert ReactiveElement, "ReactiveElement not exported" - - run_test "exports tohtml helper", -> - import tohtml from require 'lib.component' - assert 'function' == (type tohtml), "tohtml not exported" - - run_test "exports append helper", -> - import append from require 'lib.component' - assert 'function' == (type append), "append not exported" - - run_test "exports text helper", -> - import text from require 'lib.component' - assert 'function' == (type text), "text not exported" - - node = text 'a test string' - assert (js.instanceof node, js.global.Node), "expected text to generate a Node" - assert node.data == 'a test string', "expected text to store the string" - - run_test "text joins multiple arguments", -> - import text from require 'lib.component' - - node = text 'a', 'test', 'string' - assert node.data == 'a test string', "expected text to join arguments with spaces" - - run_test "exports elements table", -> - import elements from require 'lib.component' - - assert (type elements.div!) == 'table', "expected to build element with elements.div!" - assert (type elements.madeup!) == 'table', "expected to build element with elements.madeup!" - - run_test = test_group 'ReactiveVar' - - run_test "stores a value", -> - reactive = ReactiveVar 'test' - assert 'test' == reactive\get!, "expected x to be 'test'" - - run_test "propagates updates", -> - local done - - reactive = ReactiveVar 'test' - reactive\subscribe coroutine.wrap (next) -> - assert next == 'toast', "expected next to be 'toast'" - assert coroutine.yield! == 'cheese', "expected next to be 'cheese'" - done = true - - reactive\set 'toast' - assert 'toast' == reactive\get!, "expected #get to return 'toast'" - reactive\set 'cheese' - assert done, "expected to reach the end" - - run_test "passed old value as well", -> - local done - - reactive = ReactiveVar 1 - reactive\subscribe coroutine.wrap (next, last) -> - assert last == 1, "expected last:1 to be 1" - next, last = coroutine.yield! - assert last == 2, "expected last:2 to be 2" - done = true - - reactive\set 2 - reactive\set 3 - assert done, "expected to reach the end" - - run_test "provides transform shorthand", -> - local done - - reactive = ReactiveVar 1 - reactive\subscribe coroutine.wrap (next, last) -> - assert last == 1, "expected last:1 to be 1" - next, last = coroutine.yield! - assert last == 2, "expected last:2 to be 2" - done = true - - add_one = (a) -> a + 1 - reactive\transform add_one - reactive\transform add_one - assert done, "expected to reach the end" - - run_test "#subscribe returns function to unsubscribe", -> - calls = 0 - - reactive = ReactiveVar 1 - unsub = reactive\subscribe coroutine.wrap () -> - calls += 1 - coroutine.yield! - calls += 1 - coroutine.yield! - calls += 1 - - assert 'function' == (type unsub), "expected to receive a function" - - reactive\set 2 - reactive\set 3 - assert calls == 2, "wat" - - unsub! - reactive\set 4 - assert calls == 2, "expected to stop receiving updates" - - run_test "tracks multiple subscriptions at once", -> - reactive = ReactiveVar 'test' - unsub = reactive\subscribe coroutine.wrap (next) -> - assert next == 'toast', "expected next to be toast" - next = coroutine.yield! - assert next == 'cheese', "expected next to be cheese" - coroutine.yield! - error "expected not to get here" - - reactive\set 'toast' - - local done - reactive\subscribe coroutine.wrap (next) -> - assert next == 'cheese', "expected next to be cheese" - next = coroutine.yield! - assert next == 'test', "expected next to be test" - done = true - - reactive\set 'cheese' - unsub! - reactive\set 'test' - assert done, "expected to reach the end" - - run_test = test_group 'ReactiveElement' - - run_test "creates a HTML element", -> - elem = ReactiveElement 'span' - assert elem.node and elem.node.localName == 'span', "expected Node to be a " - - run_test "sets attributes from a table arg", -> - elem = ReactiveElement 'span', class: 'never' - assert elem.node.class == 'never', "expected class to be 'never'" - - run_test "appends Nodes from arguments", -> - e_div, e_pre = div!, pre! - elem = ReactiveElement 'span', e_div, e_pre - assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" - assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem" - - run_test "can append ReactiveElements and text", -> - e_div = ReactiveElement 'div' - elem = ReactiveElement 'div', e_div, 'testtext' - assert elem.node.firstElementChild == e_div.node, "expected div to be the first child of elem" - assert elem.node.lastChild.data == 'testtext', "expected last child of elem to be 'testtext'" - - run_test "accepts attributes after children", -> - e_div = div! - elem = ReactiveElement 'div', e_div, class: 'test' - assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" - assert elem.node.class == 'test', "expected class to be 'test'" - - run_test "allows mixing attributes and children in a single table", -> - e_div, e_pre = div!, pre! - elem = ReactiveElement 'div', { class: 'test', e_div, e_pre } - assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" - assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem" - assert elem.node.class == 'test', "expected class to be 'test'" - - run_test "can unwrap and track attributes from ReactiveVars", -> - klass = ReactiveVar 'test' - elem = ReactiveElement 'div', class: klass - assert elem.node.class == 'test', "expected class to be 'test'" - klass\set 'toast' - assert elem.node.class == 'toast', "expected class to be 'toast'" - - run_test "can unwrap and track children from ReactiveVars", -> - child = ReactiveVar h1 'test' - elem = ReactiveElement 'div', child, pre 'fixed' - assert elem.node.firstElementChild.localName == 'h1', "expected first child to be h1" - assert elem.node.childElementCount == 2, "expected node to have two children" - child\set div 'toast' - assert elem.node.firstElementChild.localName == 'div', "expected first child to be div" - assert elem.node.childElementCount == 2, "expected node to have two children" - - run_test "warns when appending a string from a ReactiveVar", -> - import text from require 'lib.component' - - str = ReactiveVar 'test' - elem = ReactiveElement 'div', str - expect 'cannot replace string node', 'expected error', str\set, 'string too' - elem\destroy! - - elem = ReactiveElement 'div', str\map text - str\set 'this is text' diff --git a/app/todo.moon b/app/todo.moon deleted file mode 100644 index f2baa50..0000000 --- a/app/todo.moon +++ /dev/null @@ -1,35 +0,0 @@ -on_client -> - import ReactiveVar, append, text, elements from require 'lib.component' - import div, form, span, h3, a, input, textarea, button from elements - - parent = div! - todoItem = (desc, done) -> - -- convert into reactive data sources - desc, done = (ReactiveVar desc), ReactiveVar done - with me = div style: - margin: '8px' - padding: '8px' - background: '#eeeeee' - \append h3 (desc\map text), style: 'margin: 0;' - \append span done\map (done) -> text if done then 'done' else 'not done yet' - \append input type: 'checkbox', checked: done, onchange: (e) => done\set e.target.checked - \append a (text 'delete'), href: '#', onclick: (e) => parent\remove me - - parent\append todoItem 'write a Component System' - parent\append todoItem 'eat Lasagna', true - - desc = ReactiveVar 'start' - form = with form { - action: '' - style: - margin: '2px' - onsubmit: (e) => - e\preventDefault! - parent\append todoItem desc\get! - desc\set '' - } - \append input type: 'text', value: desc, onchange: (e) => desc\set e.target.value - \append input type: 'submit', value: 'add' - - append parent - append form diff --git a/lib/canvasapp.client.moon b/lib/canvasapp.client.moon deleted file mode 100644 index 71ffa59..0000000 --- a/lib/canvasapp.client.moon +++ /dev/null @@ -1,88 +0,0 @@ -window = js.global -js = require 'js' - -import a, canvas, div, button, script from require 'lib.dom' - -class CanvasApp - width: 500 - height: 400 - new: (show_menu=false, @paused) => - @canvas = canvas width: @width, height: @height - - @ctx = @canvas\getContext '2d' - @time = 0 - - @canvas.tabIndex = 0 - @canvas\addEventListener 'click', (_, e) -> @click and @click e.offsetX, e.offsetY, e.button - @canvas\addEventListener 'keydown', (_, e) -> @keydown and @keydown e.key, e.code - - lastMillis = window.performance\now! - - animationFrame = (_, millis) -> - @update (millis - lastMillis) / 1000 - @ctx\resetTransform! - @draw! - lastMillis = millis - - if not @paused - window\setTimeout (-> - window\requestAnimationFrame animationFrame - ), 0 - window\requestAnimationFrame animationFrame - - @node = if show_menu - div { - className: 'canvas_app' - @canvas - div { - className: 'overlay', - button 'render 30fps', onclick: -> @\render 30 - button 'render 60fps', onclick: -> @\render 60 - } - } - else - @canvas - - - update: (dt) => - @time += dt - - if @length and @time > @length - @time -= @length - true - - render: (fps=60) => - assert @length, 'cannot render CanvasApp without length set' - @paused = true - - actual_render = -> - writer = js.new window.Whammy.Video, fps - - doFrame = -> - done = @update 1/fps - @ctx\resetTransform! - @draw! - - writer\add @canvas - - if done or @time >= @length - blob = writer\compile! - name = "#{@@__name}_#{fps}fps.webm" - @node.lastChild\appendChild a name, download: name, href: window.URL\createObjectURL blob - else - window\setTimeout doFrame - - @time = 0 - doFrame! - - if window.Whammy - actual_render! - else - window.global = window.global or window - document.body\appendChild script - onload: actual_render - src: 'https://cdn.jsdelivr.net/npm/whammy@0.0.1/whammy.min.js' - -{ - :CanvasApp -} diff --git a/lib/color.moon b/lib/color.moon deleted file mode 100644 index 6233b47..0000000 --- a/lib/color.moon +++ /dev/null @@ -1,20 +0,0 @@ -rgb = (r, g, b) -> - r, g, b = table.unpack r if 'table' == type r - "rgb(#{r * 255}, #{g * 255}, #{b * 255})" - -rgba = (r, g, b, a) -> - r, g, b, a = table.unpack r if 'table' == type r - "rgba(#{r * 255}, #{g * 255}, #{b * 255}, #{a or 1})" - -hsl = (h, s, l) -> - h, s, l = table.unpack h if 'table' == type h - "hsl(#{h * 360}, #{s * 100}%, #{l * 100}%)" - -hsla = (h, s, l, a) -> - h, s, l, a = table.unpack h if 'table' == type h - "hsla(#{h * 360}, #{s * 100}%, #{l * 100}%, #{a or 1})" - -{ - :rgb, :rgba, - :hsl, :hsla, -} diff --git a/lib/component.client.moon b/lib/component.client.moon deleted file mode 100644 index ffeb084..0000000 --- a/lib/component.client.moon +++ /dev/null @@ -1,158 +0,0 @@ --- convert anything to a DOM Node --- val must be one of: --- * DOM Node (instanceof window.Node) --- * MMMElement (have a .node value that is instanceof window.Node) --- * string --- note that strings won't survive identity comparisons after tohtml -tohtml = (val) -> - if 'string' == type val - return document\createTextNode val - if 'table' == type val - assert val.node, "Table doesn't have .node" - val = val.node - if 'userdata' == type val - assert (js.instanceof val, js.global.Node), "userdata is not a Node" - val - else - error "not a Node: #{val}, #{type val}" - --- overloaded append --- see tohtml for acceptable values -g_append = append -append = (value) -> g_append tohtml value - --- shorthand to form a text node from strings -text = (...) -> document\createTextNode table.concat { ... }, ' ' - -class ReactiveVar - @isinstance: (val) -> 'table' == (type val) and val.subscribe - - new: (@value) => - @listeners = setmetatable {}, __mode: 'kv' - - set: (value) => - old = @value - @value = value - for k, callback in pairs @listeners - callback @value, old - - get: => @value - - transform: (transform) => @set transform @get! - - subscribe: (callback) => - with -> @listeners[callback] = nil - @listeners[callback] = callback - - map: (transform) => - with ReactiveVar transform @value - .upstream = @subscribe (...) -> \set transform ... - --- join = do --- update = (k, new) -> (old) -> --- with copy = { k, v for k,v in pairs old } --- copy[k] = new --- --- (inputs) -> --- values = {} --- with ReactiveVar values --- for k, input in pairs inputs --- input = inputs[k] --- values[i] = input\get! --- input\subscribe (new) -> \transform update k, new - -class ReactiveElement - @isinstance: (val) -> 'table' == (type val) and val.node - - new: (element, ...) => - if 'userdata' == type element - @node = element - else - @node = document\createElement element - @_subscriptions = {} - - children = { ... } - -- attributes are last arguments but mustn't be a ReactiveVar - attributes = children[#children] - if 'table' == (type attributes) and - (not ReactiveElement.isinstance attributes) and - (not ReactiveVar.isinstance attributes) - table.remove children - else - attributes = {} - - for k,v in pairs attributes - @set k, v if 'string' == type k - - -- if there is only one argument, - -- children can be in attributes table too - if #children == 0 - children = attributes - - for child in *children - @append child - - destroy: => - for unsub in *@_subscriptions do unsub! - - set: (attr, value) => - attr = 'className' if attr == 'class' - if 'table' == (type value) and ReactiveVar.isinstance value - table.insert @_subscriptions, value\subscribe (...) -> @set attr, ... - value = value\get! - - if attr == 'style' and 'table' == type value - for k,v in pairs value - @node.style[k] = v - return - - @node[attr] = value - - prepend: (child, last) => @append child, last, 'prepend' - append: (child, last, mode='append') => - if ReactiveVar.isinstance child - table.insert @_subscriptions, child\subscribe (...) -> @append ... - child = child\get! - - if 'string' == type last - error 'cannot replace string node' - - if child == nil - if last - @remove last - return - - child = tohtml child - ok, last = pcall tohtml, last - if ok - @node\replaceChild child, last - else - switch mode - when 'append' then @node\appendChild child - when 'prepend' then @node\insertBefore child, @node.firstChild - - remove: (child) => - @node\removeChild tohtml child - if 'table' == (type child) and child.destroy - child\destroy! - -get_or_create = (elem, id, ...) -> - elem = (document\getElementById id) or elem - - with ReactiveElement elem, ... - \set 'id', id - -elements = setmetatable {}, __index: (name) => - with val = (...) -> ReactiveElement name, ... - @[name] = val - -{ - :ReactiveVar, - :ReactiveElement, - :get_or_create, --- :join, - :tohtml, - :append, - :text, - :elements, -} diff --git a/lib/component.server.moon b/lib/component.server.moon deleted file mode 100644 index 0026d32..0000000 --- a/lib/component.server.moon +++ /dev/null @@ -1,130 +0,0 @@ -import opairs from require 'lib.ordered' - -void_tags = { 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' } -void_tags = { t,t for t in *void_tags } - --- convert anything to HTML string --- val must be one of: --- * MMMElement (have a .render method that returns a string) --- * string -tohtml = (val) -> - if 'table' == type val - assert val.render, "Table doesn't have .render" - val = val\render! - if 'string' == type val - val - else - error "not a Node: #{val}, #{type val}" - --- overloaded append --- see tohtml for acceptable values -g_append = append -append = (value) -> g_append tohtml value - --- shorthand to form a text node from strings -text = (...) -> table.concat { ... }, ' ' - -class ReactiveVar - @isinstance: (val) -> 'table' == (type val) and val.subscribe - - new: (@value) => - - set: (value) => - error "attempting to update ReactiveVar serverside" - - get: => @value - - subscribe: (callback) => - warn "attempting to subscribe to ReactiveVar serverside" - - map: (transform) => - ReactiveVar transform @value - -class ReactiveElement - @isinstance: (val) -> 'table' == (type val) and val.render - - new: (element, ...) => - @element = element - @attrs = { style: {} } - @children = {} - - children = { ... } - - -- attributes are last arguments but mustn't be a ReactiveVar - attributes = children[#children] - if 'table' == (type attributes) and - (not ReactiveElement.isinstance attributes) and - (not ReactiveVar.isinstance attributes) - table.remove children - else - attributes = {} - - for k,v in pairs attributes - @set k, v if 'string' == type k - - -- if there is only one argument, - -- children can be in attributes table too - if #children == 0 - children = attributes - - for child in *children - @append child - - destroy: => - - set: (attr, value) => - if attr == 'style' and 'table' == type value - for k,v in opairs value - @attrs.style[k] = v - return - - @attrs[attr] = value - - append: (child, last) => - assert not last, "last passed to append on server" - if ReactiveVar.isinstance child - child = child\get! - - child = tohtml child - table.insert @children, child - - remove: (child) => - error "remove called serverside" - - render: => - b = "<#{@element}" - for k,v in opairs @attrs - if 'table' == type v - tmp = '' - for kk, vv in opairs v - tmp ..= "#{kk}: #{vv}; " - v = tmp - b ..= " #{k}=\"#{v}\"" - - if void_tags[@element] - assert #@children == 0, "void tag #{element} cannot have children!" - b .. ">" - else - b .. ">" - b ..= ">" .. table.concat @children, '' - b ..= "" - b - -elements = setmetatable {}, __index: (name) => - with val = (...) -> ReactiveElement name, ... - @[name] = val - -get_or_create = (elem, id, ...) -> - with ReactiveElement elem, ... - \set 'id', id - -{ - :ReactiveVar, - :ReactiveElement, - :get_or_create, - :tohtml, - :flush, - :append, - :text, - :elements, -} diff --git a/lib/dom.client.moon b/lib/dom.client.moon deleted file mode 100644 index acdbfa4..0000000 --- a/lib/dom.client.moon +++ /dev/null @@ -1,33 +0,0 @@ -element = (element) -> (...) -> - children = { ... } - - -- attributes are last arguments but mustn't be a ReactiveVar - attributes = children[#children] - if 'table' == (type attributes) and not attributes.node - table.remove children - else - attributes = {} - - with e = document\createElement element - for k,v in pairs attributes - k = 'className' if k == 'class' - if k == 'style' and 'table' == type v - for kk,vv in pairs v - e.style[kk] = vv - elseif 'string' == type k - e[k] = v - - -- if there is only one argument, - -- children can be in attributes table too - if #children == 0 - children = attributes - - for child in *children - if 'string' == type child - child = document\createTextNode child - - e\appendChild child - -setmetatable {}, __index: (name) => - with val = element name - @[name] = val diff --git a/lib/dom.server.moon b/lib/dom.server.moon deleted file mode 100644 index fe6b60c..0000000 --- a/lib/dom.server.moon +++ /dev/null @@ -1,40 +0,0 @@ -import opairs from require 'lib.ordered' - -void_tags = { 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' } -void_tags = { t,t for t in *void_tags } - -element = (element) -> (...) -> - children = { ... } - - -- attributes are last arguments but mustn't be a ReactiveVar - attributes = children[#children] - if 'table' == (type attributes) and not attributes.node - table.remove children - else - attributes = {} - - b = "<#{element}" - for k,v in opairs attributes - if k == 'style' and 'table' == type v - tmp = '' - for kk, vv in opairs v - tmp ..= "#{kk}: #{vv}; " - v = tmp - b ..= " #{k}=\"#{v}\"" - - -- if there is only one argument, - -- children can be in attributes table too - if #children == 0 - children = attributes - - if void_tags[element] - assert #children == 0, "void tag #{element} cannot have children!" - b .. ">" - else - b ..= ">" .. table.concat children, '' - b ..= "" - b - -setmetatable {}, __index: (name) => - with val = element name - @[name] = val diff --git a/lib/duct_tape.server.moon b/lib/duct_tape.server.moon deleted file mode 100644 index b019ac8..0000000 --- a/lib/duct_tape.server.moon +++ /dev/null @@ -1,130 +0,0 @@ -package.noompath = './?.moon;./?/init.moon' - -export __FNDEF_NOOM_CACHE -__FNDEF_NOOM_CACHE = setmetatable {}, __mode: 'k' - -assert MODE == 'SERVER', "duct_tape only works on the server" - -compile = require 'moonscript.compile' -parse = require 'moonscript.parse' - -import smart_node, build from require 'moonscript.types' -import dirsep from require 'moonscript.base' -import get_options, unpack from require 'moonscript.util' - -line_tables = require 'moonscript.line_tables' -lua = :loadstring, :load - -local * - -escape_str = (str) -> (str\gsub('\\', '\\\\')\gsub '"', '\\"'), nil - -deep_clone = (value) -> - if 'table' == type value - { deep_clone(k), deep_clone(v) for k,v in pairs value } - else -- number, string, boolean, etc - value - -transform_fndef = (fndef, parent, i) -> - -- compile the function to Lua separately - code, ltable, pos = compile.tree { deep_clone fndef } - if not code - return compile.format_error(ltable, pos, text) - - code = "(function() #{code} end)()" - - -- assign the lua code to __FNDEF_NOOM_CACHE[function] so it can be retrieved by `compile` - fn = -> { 'ref', 'fn' } - cache = build.chain { - base: '__FNDEF_NOOM_CACHE', - { 'index', fn! } - } - - parent[i] = build.do { - { 'declare_with_shadows', { 'fn' } } - build.assign_one fn!, fndef - build.assign_one cache, { 'string', '"', escape_str code } - -- build.assign_one cache, { 'string', '[[', code } - fn! - } - - nil - -find_fndefs = (node, p, i) -> - if node[1] == "fndef" - transform_fndef node, p, i - else - for i = 1,#node - v = node[i] - find_fndefs v, node, i if 'table' == type v - -to_lua = (text, options={}) -> - if "string" != type text - t = type text - return nil, "expecting string (got ".. t ..")" - - tree, err = parse.string text - if not tree - return nil, err - - find_fndefs tree - - code, ltable, pos = compile.tree tree, options - if not code - return nil, compile.format_error(ltable, pos, text) - - code, ltable - -noom_loader = (name, ...) -> - name_path = name\gsub "%.", dirsep - - local file, file_path - for path in package.noompath\gmatch "[^;]+" - file_path = path\gsub "?", name_path - file = io.open file_path - break if file - - if file - text = file\read "*a" - file\close! - res, err = loadstring text, "@#{file_path}" - if not res - error file_path .. ": " .. err - - return res - - return nil, "Could not find noom file" - -loadstring = (...) -> - options, str, chunk_name, mode, env = get_options ... - chunk_name or= "=(noom.loadstring)" - - code, ltable_or_err = to_lua str, options - unless code - return nil, ltable_or_err - - line_tables[chunk_name] = ltable_or_err if chunk_name - - -- the unpack prevents us from passing nil - (lua.loadstring or lua.load) code, chunk_name, unpack { mode, env } - -do - local compile - insert_loader = (pos=2) -> - -- if not package.moonpath - -- package.moonpath = create_moonpath package.path - - loaders = package.loaders or package.searchers - for loader in *loaders - return false if loader == noom_loader - - table.insert loaders, pos, noom_loader - true - - compile = (fn) -> with code = __FNDEF_NOOM_CACHE[fn] - assert code, 'cannot compile function not loaded from noomscript source.' - - { - :insert_loader - :compile - } diff --git a/lib/init.client.moon b/lib/init.client.moon deleted file mode 100644 index 144543b..0000000 --- a/lib/init.client.moon +++ /dev/null @@ -1,49 +0,0 @@ -export MODE, print, warn, relative, on_client -export window, document - -window = js.global -{ :document, :console } = window - -MODE = 'CLIENT' - -deep_tostring = (tbl, space='') -> - return tbl if 'userdata' == type tbl - - buf = space .. tostring tbl - return buf unless 'table' == type tbl - - buf = buf .. ' {\n' - for k,v in pairs tbl - buf = buf .. "#{space} [#{k}]: #{deep_tostring v, space .. ' '}\n" - buf = buf .. "#{space}}" - buf - -print = (...) -> - contents = [deep_tostring v for v in *{ ... } ] - console\log table.unpack contents - -warn = (...) -> - contents = [deep_tostring v for v in *{ ... } ] - console\warn table.unpack contents - --- package.path = '/?.client.moon.lua;/?.moon.lua;/?/init.moon.lua;/?.lua;/?/init.lua' -package.path = '/?.lua;/?/init.lua' - --- relative imports -relative = do - _require = require - - (base, sub) -> - sub = 0 unless 'number' == type sub - - for i=1, sub - base = base\match '^(.*)%.%w+$' - - (name, x) -> - name = base .. name if '.' == name\sub 1, 1 - _require name - -on_client = (f, ...) -> f ... - -if on_load - for f in *on_load do f! diff --git a/lib/init.server.moon b/lib/init.server.moon deleted file mode 100644 index cc3851d..0000000 --- a/lib/init.server.moon +++ /dev/null @@ -1,60 +0,0 @@ -export MODE, print, warn, relative, on_client -MODE = 'SERVER' - -deep_tostring = (tbl, space='') -> - buf = space .. tostring tbl - - return buf unless 'table' == type tbl - - buf = buf .. ' {\n' - for k,v in pairs tbl - buf = buf .. "#{space} [#{k}]: #{deep_tostring v, space .. ' '}\n" - buf = buf .. "#{space}}" - buf - -print = do - _print = print - (...) -> - contents = [deep_tostring v for v in *{ ... } ] - _print table.unpack contents - --- warning messages -warn = (...) -> - contents = [deep_tostring v for v in *{ ... } ] - io.stderr\write table.concat contents, '\t' - io.stderr\write '\n' - --- relative imports -relative = do - _require = require - - (base, sub) -> - sub = 0 unless 'number' == type sub - - for i=1, sub - base = base\match '^(.*)%.%w+$' - - (name, x) -> - name = base .. name if '.' == name\sub 1, 1 - _require name - -import compile, insert_loader from require 'lib.duct_tape' -insert_loader! - -on_client = (fn, ...) -> - args = {...} - -- warn code - "" - -{ - -- access / flush the appended data - flush: -> - with x = buffer - buffer = '' -} diff --git a/lib/mmmfs/browser.moon b/lib/mmmfs/browser.moon deleted file mode 100644 index ce7709f..0000000 --- a/lib/mmmfs/browser.moon +++ /dev/null @@ -1,122 +0,0 @@ -require = relative ..., 1 -import Key from require '.fileder' -import get_conversions from require '.conversion' -import ReactiveVar, get_or_create, text, elements from require 'lib.component' -import div, span, a, select, option from elements - -class Browser - new: (@root, path='/', rehydrate=false) => - -- root fileder - assert @root, 'root fileder is nil' - - -- active path - @path = ReactiveVar path - - if MODE == 'CLIENT' - -- update URL bar - @path\subscribe (path) -> - path ..= '/' unless path\match '/$' - window.history\pushState nil, '', path - - -- active fileder - -- (re)set every time @path changes - @active = @path\map @root\walk - - -- currently active property - -- (re)set to default every time @active changes - @prop = @active\map (fileder) -> - return unless fileder - (fileder\find 'mmm/dom') or next fileder.props - - -- retrieve or create the root - @dom = get_or_create 'div', 'browser-root' - - -- prepend the navbar - if MODE == 'SERVER' - @dom\append div id: 'browser-navbar' - else - @dom\prepend get_or_create 'div', 'browser-navbar', { - span 'path: ', @path\map (path) -> with div style: { display: 'inline-block' } - path_segment = (name, href) -> - a name, :href, onclick: (_, e) -> - e\preventDefault! - @navigate href - - href = '' - path = path\match '^/(.*)' - - \append path_segment 'root', '/' - - while path - name, rest = path\match '^(%w+)/(.*)' - if not name - name = path - - path = rest - href = "#{href}/#{name}" - - \append '/' - \append path_segment name, href - - span 'view property: ', @active\map (fileder) -> - onchange = (_, e) -> - @prop\set Key e.target.value - - current = @prop\get! - current = current and current\tostring! - with select :onchange, disabled: not fileder - if fileder - for key, _ in pairs fileder.props - value = key\tostring! - \append option value, :value, selected: value == current - } - - -- append or patch #browser-content - @dom\append with get_or_create 'div', 'browser-content' - \append @get_content!, (rehydrate and .node.lastChild) - - if rehydrate - -- force one rerender to set onclick handlers etc - @prop\set @prop\get! - - -- export mmm/component interface - @node = @dom.node - @render = @dom\render - - -- render #browser-content - get_content: () => - disp_error = (msg) -> span msg, style: { color: '#f00' } - @prop\map (prop) -> - active = @active\get! - - return disp_error "fileder not found!" unless active - return disp_error "property not found!" unless prop - - convert = -> - conversions = get_conversions 'mmm/dom', prop.type - value = assert (active\get prop), "value went missing?" - - return unless conversions - - for i=#conversions,1,-1 - { :inp, :out, :transform } = conversions[i] - value = transform value, active - - value - - ok, res = if MODE == 'CLIENT' - pcall convert - else - true, convert! - - if ok - res or disp_error "[no conversion path to mmm/dom]" - else - warn "error: ", res unless ok - disp_error "[unknown error displaying]" - - navigate: (new) => @path\set new - -{ - :Browser -} diff --git a/lib/mmmfs/conversion.moon b/lib/mmmfs/conversion.moon deleted file mode 100644 index 7f86aa4..0000000 --- a/lib/mmmfs/conversion.moon +++ /dev/null @@ -1,118 +0,0 @@ -import text, code from require 'lib.dom' -import tohtml from require 'lib.component' - --- limit function to one argument -single = (func) -> (val) -> func val - --- list of converts --- converts each have --- * inp - input type. can capture subtypes using `(.+)` --- * out - output type. can substitute subtypes from inp with %1, %2 etc. --- * transform - function (val: inp, fileder) -> val: out -converts = { - { - inp: 'moon -> (.+)', - out: '%1', - transform: (val, fileder) -> val fileder - }, - { - inp: 'text/plain', - out: 'mmm/dom', - transform: single text - }, - { - inp: 'alpha', - out: 'mmm/dom', - transform: single code - }, - { - inp: 'URL -> .*', - out: 'mmm/dom', - transform: single code - }, - { - inp: 'mmm/component', - out: 'mmm/dom', - transform: single tohtml - }, - { - inp: 'mmm/dom', - out: 'text/html', - transform: (node) -> if MODE == 'SERVER' then node else node.outerHTML - }, - { - inp: 'text/html', - out: 'mmm/dom', - transform: if MODE == 'SERVER' - (...) -> ... - else - (html) -> - tmp = document\createElement 'div' - tmp.innerHTML = html - if tmp.childElementCount == 1 - tmp.firstChild - else - tmp - } -} - -do - local markdown - if MODE == 'SERVER' - success, discount = pcall require, 'discount' - markdown = discount if success - else - markdown = window and window.marked and window\marked - - if markdown - table.insert converts, { - inp: 'text/markdown', - out: 'text/html', - transform: single markdown - } - -count = (base, pattern='->') -> select 2, base\gsub pattern, '' -escape_inp = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$" - --- attempt to find a conversion path from 'have' to 'want' --- * have - start type string or list of type strings --- * want - stop type string --- * limit - limit conversion amount --- returns a list of conversion steps -get_conversions = (want, have, limit=3) -> - assert have, 'need starting type(s)' - - if 'string' == type have - have = { have } - - assert #have > 0, 'need starting type(s) (list was empty)' - - iterations = limit + math.max table.unpack [count type for type in *have] - have = [{ :start, rest: start, conversions: {} } for start in *have] - - for i=1, iterations - next_have, c = {}, 1 - for { :start, :rest, :conversions } in *have - if want == rest - return conversions, start - else - for convert in *converts - inp = escape_inp convert.inp - matches = { rest\match inp } - continue unless #matches > 0 - result = rest\gsub inp, convert.out - if result - next_have[c] = { - :start, - rest: result, - conversions: { convert, table.unpack conversions } - } - c += 1 - - have = next_have - return unless #have > 0 - -{ - :converts - :get_conversions -} diff --git a/lib/mmmfs/fileder.moon b/lib/mmmfs/fileder.moon deleted file mode 100644 index c3e40f7..0000000 --- a/lib/mmmfs/fileder.moon +++ /dev/null @@ -1,155 +0,0 @@ -require = relative ..., 1 -import get_conversions from require '.conversion' - --- Key of a Fileder Property --- contains: --- * @name - key name or '' for main content --- * @type - type string (type -> type -> type) -class Key - -- instantiate from table w/ keys described above - -- or string like '@name: @type' (name optional) - new: (opts, second) => - if 'string' == type second - @name, @type = (opts or ''), second - elseif 'string' == type opts - @name, @type = opts\match '(%w+): *(.+)' - if not @name - @name = '' - @type = opts - elseif 'table' == type opts - @name = opts.name - @type = opts.type - else - error "wrong argument type: #{type opts}, #{type second}" - - -- format as a string (see constructor) - tostring: => - if @name == '' - @type - else - "#{@name}: #{@type}" - --- Fileder itself --- contains: --- * @props - Property Map (Key to Value) --- * @children - Children Array -class Fileder - -- instantiate from props and children tables - -- or mix in one table (numeric keys are children, remainder props) - -- prop-keys are passed to Key constructor - new: (props, children) => - if not children - children = for i, child in ipairs props - props[i] = nil - child - - -- automatically mount children on insert - @children = setmetatable {}, __newindex: (t, k, child) -> - rawset t, k, child - if @path == '/' - child\mount '/' - elseif @path - child\mount @path .. '/' - - -- copy children - for i, child in ipairs children - @children[i] = child - - -- automatically reify string keys on insert - @props = setmetatable {}, __newindex: (t, key, v) -> - rawset t, key, nil -- fix for fengari.io - rawset t, (Key key), v - - -- copy props - for k, v in pairs props - @props[k] = v - - -- recursively walk to and return the fileder with @path == path - -- * path - the path to walk to - walk: (path) => - -- early-out if we are outside of the path already - return unless path\match '^' .. @path - - -- gotcha - return @ if path == @path - - for child in *@children - result = child\walk path - return result if result - - -- recursively mount fileder and children at path - -- * path - the path to mount at (default: '/') - -- * mount_as - dont append own name to path - mount: (path='/', mount_as=false) => - assert not @path, "mounted twice: #{@path} and now #{path}" - - if mount_as - @path = path - else - @path = path .. @gett 'name: alpha' - - if @path == '/' - for child in *@children - child\mount '/' - else - for child in *@children - child\mount @path .. '/' - - -- recursively iterate all children (coroutine) - -- * depth - depth to stop after; 1 = yield only self (default: infinite) - iterate: (depth=0) => - coroutine.yield @ - return if depth == 1 - - for child in *@children - child\iterate depth - 1 - - -- find property key according to criteria, nil if no value or conversion path - -- * ... - arguments like Key - find: (...) => - want = Key ... - - -- filter props by name - matching = [ key for key in pairs @props when key.name == want.name ] - return unless #matching > 0 - - -- get shortest conversion path - shortest_path, start = get_conversions want.type, [ key.type for key in *matching ] - - if start - for key in *matching - if key.type == start - return key, shortest_path - - error "couldn't find key after resolution?" - - -- get property according to criteria, nil if no value or conversion path - -- * ... - arguments like Key - get: (...) => - want = Key ... - - -- find matching key and shortest conversion path - key, conversions = @find want - - if key - value = @props[key] - - -- apply conversions (in reverse order) - for i=#conversions,1,-1 - { :inp, :out, :transform } = conversions[i] - value = transform value, @ - - value, key - - -- like @get, throw if no value or conversion path - gett: (...) => - want = Key ... - - value, key = @get want - assert value, "node doesn't have value for '#{want\tostring!}'" - value, key - -{ - :Key - :Fileder -} diff --git a/lib/mmmfs/init.moon b/lib/mmmfs/init.moon deleted file mode 100644 index d0de45b..0000000 --- a/lib/mmmfs/init.moon +++ /dev/null @@ -1,34 +0,0 @@ -require = relative ... -import Key, Fileder from require '.fileder' -import Browser from require '.browser' -import tohtml from require 'lib.component' - -define_fileders = (...) -> - source_module = ... - - (...) -> - with fileder = Fileder ... - .source_module = source_module - -rehydrate = (path) -> - import Browser from require 'lib.mmmfs.browser' - root = require 'root' - root\mount! - - export BROWSER - BROWSER = Browser root, path, true - -render = (root, path) -> - export BROWSER - BROWSER = Browser root, path - - content = tohtml BROWSER - content, on_client rehydrate, path - -{ - :Key - :Fileder - :render - :define_fileders - :module_roots -} diff --git a/lib/ordered.moon b/lib/ordered.moon deleted file mode 100644 index 59c07bf..0000000 --- a/lib/ordered.moon +++ /dev/null @@ -1,27 +0,0 @@ --- ordered table iterator, for stable(r) renderers - -sort = (t, order_fn) -> - with index = [k for k,v in pairs t when 'string' == type k] - table.sort index, order_fn - --- ordered next(t) -onext = (state, key) -> - state.i += 1 - { :t, :index, :i } = state - - if key = index[i] - key, t[key] - --- ordered pairs(t). order_fn is optional; see table.sort -opairs = (t, order_fn) -> - state = { - :t, - i: 0, - index: sort t, order_fn - } - onext, state, nil - -{ - :onext - :opairs -} diff --git a/mmm/canvasapp.client.moon b/mmm/canvasapp.client.moon new file mode 100644 index 0000000..40f2423 --- /dev/null +++ b/mmm/canvasapp.client.moon @@ -0,0 +1,88 @@ +window = js.global +js = require 'js' + +import a, canvas, div, button, script from require 'mmm.dom' + +class CanvasApp + width: 500 + height: 400 + new: (show_menu=false, @paused) => + @canvas = canvas width: @width, height: @height + + @ctx = @canvas\getContext '2d' + @time = 0 + + @canvas.tabIndex = 0 + @canvas\addEventListener 'click', (_, e) -> @click and @click e.offsetX, e.offsetY, e.button + @canvas\addEventListener 'keydown', (_, e) -> @keydown and @keydown e.key, e.code + + lastMillis = window.performance\now! + + animationFrame = (_, millis) -> + @update (millis - lastMillis) / 1000 + @ctx\resetTransform! + @draw! + lastMillis = millis + + if not @paused + window\setTimeout (-> + window\requestAnimationFrame animationFrame + ), 0 + window\requestAnimationFrame animationFrame + + @node = if show_menu + div { + className: 'canvas_app' + @canvas + div { + className: 'overlay', + button 'render 30fps', onclick: -> @\render 30 + button 'render 60fps', onclick: -> @\render 60 + } + } + else + @canvas + + + update: (dt) => + @time += dt + + if @length and @time > @length + @time -= @length + true + + render: (fps=60) => + assert @length, 'cannot render CanvasApp without length set' + @paused = true + + actual_render = -> + writer = js.new window.Whammy.Video, fps + + doFrame = -> + done = @update 1/fps + @ctx\resetTransform! + @draw! + + writer\add @canvas + + if done or @time >= @length + blob = writer\compile! + name = "#{@@__name}_#{fps}fps.webm" + @node.lastChild\appendChild a name, download: name, href: window.URL\createObjectURL blob + else + window\setTimeout doFrame + + @time = 0 + doFrame! + + if window.Whammy + actual_render! + else + window.global = window.global or window + document.body\appendChild script + onload: actual_render + src: 'https://cdn.jsdelivr.net/npm/whammy@0.0.1/whammy.min.js' + +{ + :CanvasApp +} diff --git a/mmm/color.moon b/mmm/color.moon new file mode 100644 index 0000000..6233b47 --- /dev/null +++ b/mmm/color.moon @@ -0,0 +1,20 @@ +rgb = (r, g, b) -> + r, g, b = table.unpack r if 'table' == type r + "rgb(#{r * 255}, #{g * 255}, #{b * 255})" + +rgba = (r, g, b, a) -> + r, g, b, a = table.unpack r if 'table' == type r + "rgba(#{r * 255}, #{g * 255}, #{b * 255}, #{a or 1})" + +hsl = (h, s, l) -> + h, s, l = table.unpack h if 'table' == type h + "hsl(#{h * 360}, #{s * 100}%, #{l * 100}%)" + +hsla = (h, s, l, a) -> + h, s, l, a = table.unpack h if 'table' == type h + "hsla(#{h * 360}, #{s * 100}%, #{l * 100}%, #{a or 1})" + +{ + :rgb, :rgba, + :hsl, :hsla, +} diff --git a/mmm/component/init.client.moon b/mmm/component/init.client.moon new file mode 100644 index 0000000..193fd94 --- /dev/null +++ b/mmm/component/init.client.moon @@ -0,0 +1,152 @@ +-- convert anything to a DOM Node +-- val must be one of: +-- * DOM Node (instanceof window.Node) +-- * MMMElement (have a .node value that is instanceof window.Node) +-- * string +-- note that strings won't survive identity comparisons after tohtml +tohtml = (val) -> + if 'string' == type val + return document\createTextNode val + if 'table' == type val + assert val.node, "Table doesn't have .node" + val = val.node + if 'userdata' == type val + assert (js.instanceof val, js.global.Node), "userdata is not a Node" + val + else + error "not a Node: #{val}, #{type val}" + +-- shorthand to form a text node from strings +text = (...) -> document\createTextNode table.concat { ... }, ' ' + +class ReactiveVar + @isinstance: (val) -> 'table' == (type val) and val.subscribe + + new: (@value) => + @listeners = setmetatable {}, __mode: 'kv' + + set: (value) => + old = @value + @value = value + for k, callback in pairs @listeners + callback @value, old + + get: => @value + + transform: (transform) => @set transform @get! + + subscribe: (callback) => + with -> @listeners[callback] = nil + @listeners[callback] = callback + + map: (transform) => + with ReactiveVar transform @value + .upstream = @subscribe (...) -> \set transform ... + +-- join = do +-- update = (k, new) -> (old) -> +-- with copy = { k, v for k,v in pairs old } +-- copy[k] = new +-- +-- (inputs) -> +-- values = {} +-- with ReactiveVar values +-- for k, input in pairs inputs +-- input = inputs[k] +-- values[i] = input\get! +-- input\subscribe (new) -> \transform update k, new + +class ReactiveElement + @isinstance: (val) -> 'table' == (type val) and val.node + + new: (element, ...) => + if 'userdata' == type element + @node = element + else + @node = document\createElement element + @_subscriptions = {} + + children = { ... } + -- attributes are last arguments but mustn't be a ReactiveVar + attributes = children[#children] + if 'table' == (type attributes) and + (not ReactiveElement.isinstance attributes) and + (not ReactiveVar.isinstance attributes) + table.remove children + else + attributes = {} + + for k,v in pairs attributes + @set k, v if 'string' == type k + + -- if there is only one argument, + -- children can be in attributes table too + if #children == 0 + children = attributes + + for child in *children + @append child + + destroy: => + for unsub in *@_subscriptions do unsub! + + set: (attr, value) => + attr = 'className' if attr == 'class' + if 'table' == (type value) and ReactiveVar.isinstance value + table.insert @_subscriptions, value\subscribe (...) -> @set attr, ... + value = value\get! + + if attr == 'style' and 'table' == type value + for k,v in pairs value + @node.style[k] = v + return + + @node[attr] = value + + prepend: (child, last) => @append child, last, 'prepend' + append: (child, last, mode='append') => + if ReactiveVar.isinstance child + table.insert @_subscriptions, child\subscribe (...) -> @append ... + child = child\get! + + if 'string' == type last + error 'cannot replace string node' + + if child == nil + if last + @remove last + return + + child = tohtml child + ok, last = pcall tohtml, last + if ok + @node\replaceChild child, last + else + switch mode + when 'append' then @node\appendChild child + when 'prepend' then @node\insertBefore child, @node.firstChild + + remove: (child) => + @node\removeChild tohtml child + if 'table' == (type child) and child.destroy + child\destroy! + +get_or_create = (elem, id, ...) -> + elem = (document\getElementById id) or elem + + with ReactiveElement elem, ... + \set 'id', id + +elements = setmetatable {}, __index: (name) => + with val = (...) -> ReactiveElement name, ... + @[name] = val + +{ + :ReactiveVar, + :ReactiveElement, + :get_or_create, +-- :join, + :tohtml, + :text, + :elements, +} diff --git a/mmm/component/init.server.moon b/mmm/component/init.server.moon new file mode 100644 index 0000000..b84b8d5 --- /dev/null +++ b/mmm/component/init.server.moon @@ -0,0 +1,123 @@ +import opairs from require 'mmm.ordered' + +void_tags = { 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' } +void_tags = { t,t for t in *void_tags } + +-- convert anything to HTML string +-- val must be one of: +-- * MMMElement (have a .render method that returns a string) +-- * string +tohtml = (val) -> + if 'table' == type val + assert val.render, "Table doesn't have .render" + val = val\render! + if 'string' == type val + val + else + error "not a Node: #{val}, #{type val}" + +-- shorthand to form a text node from strings +text = (...) -> table.concat { ... }, ' ' + +class ReactiveVar + @isinstance: (val) -> 'table' == (type val) and val.subscribe + + new: (@value) => + + set: (value) => + error "attempting to update ReactiveVar serverside" + + get: => @value + + subscribe: (callback) => + warn "attempting to subscribe to ReactiveVar serverside" + + map: (transform) => + ReactiveVar transform @value + +class ReactiveElement + @isinstance: (val) -> 'table' == (type val) and val.render + + new: (element, ...) => + @element = element + @attrs = { style: {} } + @children = {} + + children = { ... } + + -- attributes are last arguments but mustn't be a ReactiveVar + attributes = children[#children] + if 'table' == (type attributes) and + (not ReactiveElement.isinstance attributes) and + (not ReactiveVar.isinstance attributes) + table.remove children + else + attributes = {} + + for k,v in pairs attributes + @set k, v if 'string' == type k + + -- if there is only one argument, + -- children can be in attributes table too + if #children == 0 + children = attributes + + for child in *children + @append child + + destroy: => + + set: (attr, value) => + if attr == 'style' and 'table' == type value + for k,v in opairs value + @attrs.style[k] = v + return + + @attrs[attr] = value + + append: (child, last) => + assert not last, "last passed to append on server" + if ReactiveVar.isinstance child + child = child\get! + + child = tohtml child + table.insert @children, child + + remove: (child) => + error "remove called serverside" + + render: => + b = "<#{@element}" + for k,v in opairs @attrs + if 'table' == type v + tmp = '' + for kk, vv in opairs v + tmp ..= "#{kk}: #{vv}; " + v = tmp + b ..= " #{k}=\"#{v}\"" + + if void_tags[@element] + assert #@children == 0, "void tag #{element} cannot have children!" + b .. ">" + else + b .. ">" + b ..= ">" .. table.concat @children, '' + b ..= "" + b + +elements = setmetatable {}, __index: (name) => + with val = (...) -> ReactiveElement name, ... + @[name] = val + +get_or_create = (elem, id, ...) -> + with ReactiveElement elem, ... + \set 'id', id + +{ + :ReactiveVar, + :ReactiveElement, + :get_or_create, + :tohtml, + :text, + :elements, +} diff --git a/mmm/dom/init.client.moon b/mmm/dom/init.client.moon new file mode 100644 index 0000000..acdbfa4 --- /dev/null +++ b/mmm/dom/init.client.moon @@ -0,0 +1,33 @@ +element = (element) -> (...) -> + children = { ... } + + -- attributes are last arguments but mustn't be a ReactiveVar + attributes = children[#children] + if 'table' == (type attributes) and not attributes.node + table.remove children + else + attributes = {} + + with e = document\createElement element + for k,v in pairs attributes + k = 'className' if k == 'class' + if k == 'style' and 'table' == type v + for kk,vv in pairs v + e.style[kk] = vv + elseif 'string' == type k + e[k] = v + + -- if there is only one argument, + -- children can be in attributes table too + if #children == 0 + children = attributes + + for child in *children + if 'string' == type child + child = document\createTextNode child + + e\appendChild child + +setmetatable {}, __index: (name) => + with val = element name + @[name] = val diff --git a/mmm/dom/init.server.moon b/mmm/dom/init.server.moon new file mode 100644 index 0000000..b0a8c85 --- /dev/null +++ b/mmm/dom/init.server.moon @@ -0,0 +1,40 @@ +import opairs from require 'mmm.ordered' + +void_tags = { 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' } +void_tags = { t,t for t in *void_tags } + +element = (element) -> (...) -> + children = { ... } + + -- attributes are last arguments but mustn't be a ReactiveVar + attributes = children[#children] + if 'table' == (type attributes) and not attributes.node + table.remove children + else + attributes = {} + + b = "<#{element}" + for k,v in opairs attributes + if k == 'style' and 'table' == type v + tmp = '' + for kk, vv in opairs v + tmp ..= "#{kk}: #{vv}; " + v = tmp + b ..= " #{k}=\"#{v}\"" + + -- if there is only one argument, + -- children can be in attributes table too + if #children == 0 + children = attributes + + if void_tags[element] + assert #children == 0, "void tag #{element} cannot have children!" + b .. ">" + else + b ..= ">" .. table.concat children, '' + b ..= "" + b + +setmetatable {}, __index: (name) => + with val = element name + @[name] = val diff --git a/mmm/init.client.moon b/mmm/init.client.moon new file mode 100644 index 0000000..144543b --- /dev/null +++ b/mmm/init.client.moon @@ -0,0 +1,49 @@ +export MODE, print, warn, relative, on_client +export window, document + +window = js.global +{ :document, :console } = window + +MODE = 'CLIENT' + +deep_tostring = (tbl, space='') -> + return tbl if 'userdata' == type tbl + + buf = space .. tostring tbl + return buf unless 'table' == type tbl + + buf = buf .. ' {\n' + for k,v in pairs tbl + buf = buf .. "#{space} [#{k}]: #{deep_tostring v, space .. ' '}\n" + buf = buf .. "#{space}}" + buf + +print = (...) -> + contents = [deep_tostring v for v in *{ ... } ] + console\log table.unpack contents + +warn = (...) -> + contents = [deep_tostring v for v in *{ ... } ] + console\warn table.unpack contents + +-- package.path = '/?.client.moon.lua;/?.moon.lua;/?/init.moon.lua;/?.lua;/?/init.lua' +package.path = '/?.lua;/?/init.lua' + +-- relative imports +relative = do + _require = require + + (base, sub) -> + sub = 0 unless 'number' == type sub + + for i=1, sub + base = base\match '^(.*)%.%w+$' + + (name, x) -> + name = base .. name if '.' == name\sub 1, 1 + _require name + +on_client = (f, ...) -> f ... + +if on_load + for f in *on_load do f! diff --git a/mmm/init.server.moon b/mmm/init.server.moon new file mode 100644 index 0000000..f87596e --- /dev/null +++ b/mmm/init.server.moon @@ -0,0 +1,46 @@ +export MODE, print, warn, relative, on_client +MODE = 'SERVER' + +deep_tostring = (tbl, space='') -> + buf = space .. tostring tbl + + return buf unless 'table' == type tbl + + buf = buf .. ' {\n' + for k,v in pairs tbl + buf = buf .. "#{space} [#{k}]: #{deep_tostring v, space .. ' '}\n" + buf = buf .. "#{space}}" + buf + +print = do + _print = print + (...) -> + contents = [deep_tostring v for v in *{ ... } ] + _print table.unpack contents + +-- warning messages +warn = (...) -> + contents = [deep_tostring v for v in *{ ... } ] + io.stderr\write table.concat contents, '\t' + io.stderr\write '\n' + +-- relative imports +relative = do + _require = require + + (base, sub) -> + sub = 0 unless 'number' == type sub + + for i=1, sub + base = base\match '^(.*)%.%w+$' + + (name, x) -> + name = base .. name if '.' == name\sub 1, 1 + _require name + +{ + -- access / flush the appended data + flush: -> + with x = buffer + buffer = '' +} diff --git a/mmm/mmmfs/browser.moon b/mmm/mmmfs/browser.moon new file mode 100644 index 0000000..b057573 --- /dev/null +++ b/mmm/mmmfs/browser.moon @@ -0,0 +1,122 @@ +require = relative ..., 1 +import Key from require '.fileder' +import get_conversions from require '.conversion' +import ReactiveVar, get_or_create, text, elements from require 'mmm.component' +import div, span, a, select, option from elements + +class Browser + new: (@root, path='/', rehydrate=false) => + -- root fileder + assert @root, 'root fileder is nil' + + -- active path + @path = ReactiveVar path + + if MODE == 'CLIENT' + -- update URL bar + @path\subscribe (path) -> + path ..= '/' unless path\match '/$' + window.history\pushState nil, '', path + + -- active fileder + -- (re)set every time @path changes + @active = @path\map @root\walk + + -- currently active property + -- (re)set to default every time @active changes + @prop = @active\map (fileder) -> + return unless fileder + (fileder\find 'mmm/dom') or next fileder.props + + -- retrieve or create the root + @dom = get_or_create 'div', 'browser-root' + + -- prepend the navbar + if MODE == 'SERVER' + @dom\append div id: 'browser-navbar' + else + @dom\prepend get_or_create 'div', 'browser-navbar', { + span 'path: ', @path\map (path) -> with div style: { display: 'inline-block' } + path_segment = (name, href) -> + a name, :href, onclick: (_, e) -> + e\preventDefault! + @navigate href + + href = '' + path = path\match '^/(.*)' + + \append path_segment 'root', '/' + + while path + name, rest = path\match '^(%w+)/(.*)' + if not name + name = path + + path = rest + href = "#{href}/#{name}" + + \append '/' + \append path_segment name, href + + span 'view property: ', @active\map (fileder) -> + onchange = (_, e) -> + @prop\set Key e.target.value + + current = @prop\get! + current = current and current\tostring! + with select :onchange, disabled: not fileder + if fileder + for key, _ in pairs fileder.props + value = key\tostring! + \append option value, :value, selected: value == current + } + + -- append or patch #browser-content + @dom\append with get_or_create 'div', 'browser-content' + \append @get_content!, (rehydrate and .node.lastChild) + + if rehydrate + -- force one rerender to set onclick handlers etc + @prop\set @prop\get! + + -- export mmm/component interface + @node = @dom.node + @render = @dom\render + + -- render #browser-content + get_content: () => + disp_error = (msg) -> span msg, style: { color: '#f00' } + @prop\map (prop) -> + active = @active\get! + + return disp_error "fileder not found!" unless active + return disp_error "property not found!" unless prop + + convert = -> + conversions = get_conversions 'mmm/dom', prop.type + value = assert (active\get prop), "value went missing?" + + return unless conversions + + for i=#conversions,1,-1 + { :inp, :out, :transform } = conversions[i] + value = transform value, active + + value + + ok, res = if MODE == 'CLIENT' + pcall convert + else + true, convert! + + if ok + res or disp_error "[no conversion path to mmm/dom]" + else + warn "error: ", res unless ok + disp_error "[unknown error displaying]" + + navigate: (new) => @path\set new + +{ + :Browser +} diff --git a/mmm/mmmfs/conversion.moon b/mmm/mmmfs/conversion.moon new file mode 100644 index 0000000..5fdb4c2 --- /dev/null +++ b/mmm/mmmfs/conversion.moon @@ -0,0 +1,118 @@ +import text, code from require 'mmm.dom' +import tohtml from require 'mmm.component' + +-- limit function to one argument +single = (func) -> (val) -> func val + +-- list of converts +-- converts each have +-- * inp - input type. can capture subtypes using `(.+)` +-- * out - output type. can substitute subtypes from inp with %1, %2 etc. +-- * transform - function (val: inp, fileder) -> val: out +converts = { + { + inp: 'moon -> (.+)', + out: '%1', + transform: (val, fileder) -> val fileder + }, + { + inp: 'text/plain', + out: 'mmm/dom', + transform: single text + }, + { + inp: 'alpha', + out: 'mmm/dom', + transform: single code + }, + { + inp: 'URL -> .*', + out: 'mmm/dom', + transform: single code + }, + { + inp: 'mmm/component', + out: 'mmm/dom', + transform: single tohtml + }, + { + inp: 'mmm/dom', + out: 'text/html', + transform: (node) -> if MODE == 'SERVER' then node else node.outerHTML + }, + { + inp: 'text/html', + out: 'mmm/dom', + transform: if MODE == 'SERVER' + (...) -> ... + else + (html) -> + tmp = document\createElement 'div' + tmp.innerHTML = html + if tmp.childElementCount == 1 + tmp.firstChild + else + tmp + } +} + +do + local markdown + if MODE == 'SERVER' + success, discount = pcall require, 'discount' + markdown = discount if success + else + markdown = window and window.marked and window\marked + + if markdown + table.insert converts, { + inp: 'text/markdown', + out: 'text/html', + transform: single markdown + } + +count = (base, pattern='->') -> select 2, base\gsub pattern, '' +escape_inp = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$" + +-- attempt to find a conversion path from 'have' to 'want' +-- * have - start type string or list of type strings +-- * want - stop type string +-- * limit - limit conversion amount +-- returns a list of conversion steps +get_conversions = (want, have, limit=3) -> + assert have, 'need starting type(s)' + + if 'string' == type have + have = { have } + + assert #have > 0, 'need starting type(s) (list was empty)' + + iterations = limit + math.max table.unpack [count type for type in *have] + have = [{ :start, rest: start, conversions: {} } for start in *have] + + for i=1, iterations + next_have, c = {}, 1 + for { :start, :rest, :conversions } in *have + if want == rest + return conversions, start + else + for convert in *converts + inp = escape_inp convert.inp + matches = { rest\match inp } + continue unless #matches > 0 + result = rest\gsub inp, convert.out + if result + next_have[c] = { + :start, + rest: result, + conversions: { convert, table.unpack conversions } + } + c += 1 + + have = next_have + return unless #have > 0 + +{ + :converts + :get_conversions +} diff --git a/mmm/mmmfs/fileder.moon b/mmm/mmmfs/fileder.moon new file mode 100644 index 0000000..c3e40f7 --- /dev/null +++ b/mmm/mmmfs/fileder.moon @@ -0,0 +1,155 @@ +require = relative ..., 1 +import get_conversions from require '.conversion' + +-- Key of a Fileder Property +-- contains: +-- * @name - key name or '' for main content +-- * @type - type string (type -> type -> type) +class Key + -- instantiate from table w/ keys described above + -- or string like '@name: @type' (name optional) + new: (opts, second) => + if 'string' == type second + @name, @type = (opts or ''), second + elseif 'string' == type opts + @name, @type = opts\match '(%w+): *(.+)' + if not @name + @name = '' + @type = opts + elseif 'table' == type opts + @name = opts.name + @type = opts.type + else + error "wrong argument type: #{type opts}, #{type second}" + + -- format as a string (see constructor) + tostring: => + if @name == '' + @type + else + "#{@name}: #{@type}" + +-- Fileder itself +-- contains: +-- * @props - Property Map (Key to Value) +-- * @children - Children Array +class Fileder + -- instantiate from props and children tables + -- or mix in one table (numeric keys are children, remainder props) + -- prop-keys are passed to Key constructor + new: (props, children) => + if not children + children = for i, child in ipairs props + props[i] = nil + child + + -- automatically mount children on insert + @children = setmetatable {}, __newindex: (t, k, child) -> + rawset t, k, child + if @path == '/' + child\mount '/' + elseif @path + child\mount @path .. '/' + + -- copy children + for i, child in ipairs children + @children[i] = child + + -- automatically reify string keys on insert + @props = setmetatable {}, __newindex: (t, key, v) -> + rawset t, key, nil -- fix for fengari.io + rawset t, (Key key), v + + -- copy props + for k, v in pairs props + @props[k] = v + + -- recursively walk to and return the fileder with @path == path + -- * path - the path to walk to + walk: (path) => + -- early-out if we are outside of the path already + return unless path\match '^' .. @path + + -- gotcha + return @ if path == @path + + for child in *@children + result = child\walk path + return result if result + + -- recursively mount fileder and children at path + -- * path - the path to mount at (default: '/') + -- * mount_as - dont append own name to path + mount: (path='/', mount_as=false) => + assert not @path, "mounted twice: #{@path} and now #{path}" + + if mount_as + @path = path + else + @path = path .. @gett 'name: alpha' + + if @path == '/' + for child in *@children + child\mount '/' + else + for child in *@children + child\mount @path .. '/' + + -- recursively iterate all children (coroutine) + -- * depth - depth to stop after; 1 = yield only self (default: infinite) + iterate: (depth=0) => + coroutine.yield @ + return if depth == 1 + + for child in *@children + child\iterate depth - 1 + + -- find property key according to criteria, nil if no value or conversion path + -- * ... - arguments like Key + find: (...) => + want = Key ... + + -- filter props by name + matching = [ key for key in pairs @props when key.name == want.name ] + return unless #matching > 0 + + -- get shortest conversion path + shortest_path, start = get_conversions want.type, [ key.type for key in *matching ] + + if start + for key in *matching + if key.type == start + return key, shortest_path + + error "couldn't find key after resolution?" + + -- get property according to criteria, nil if no value or conversion path + -- * ... - arguments like Key + get: (...) => + want = Key ... + + -- find matching key and shortest conversion path + key, conversions = @find want + + if key + value = @props[key] + + -- apply conversions (in reverse order) + for i=#conversions,1,-1 + { :inp, :out, :transform } = conversions[i] + value = transform value, @ + + value, key + + -- like @get, throw if no value or conversion path + gett: (...) => + want = Key ... + + value, key = @get want + assert value, "node doesn't have value for '#{want\tostring!}'" + value, key + +{ + :Key + :Fileder +} diff --git a/mmm/mmmfs/init.moon b/mmm/mmmfs/init.moon new file mode 100644 index 0000000..52d5da8 --- /dev/null +++ b/mmm/mmmfs/init.moon @@ -0,0 +1,40 @@ +require = relative ... +import Key, Fileder from require '.fileder' +import Browser from require '.browser' +import tohtml from require 'mmm.component' + +define_fileders = (...) -> + source_module = ... + + (...) -> + with fileder = Fileder ... + .source_module = source_module + +render = (root, path) -> + export BROWSER + BROWSER = Browser root, path + + content = tohtml BROWSER + + rehydrate = " +" + content, rehydrate + +{ + :Key + :Fileder + :Browser + :render + :define_fileders + :module_roots +} diff --git a/mmm/ordered.moon b/mmm/ordered.moon new file mode 100644 index 0000000..59c07bf --- /dev/null +++ b/mmm/ordered.moon @@ -0,0 +1,27 @@ +-- ordered table iterator, for stable(r) renderers + +sort = (t, order_fn) -> + with index = [k for k,v in pairs t when 'string' == type k] + table.sort index, order_fn + +-- ordered next(t) +onext = (state, key) -> + state.i += 1 + { :t, :index, :i } = state + + if key = index[i] + key, t[key] + +-- ordered pairs(t). order_fn is optional; see table.sort +opairs = (t, order_fn) -> + state = { + :t, + i: 0, + index: sort t, order_fn + } + onext, state, nil + +{ + :onext + :opairs +} diff --git a/render.moon b/render.moon index 1246999..8098ba7 100644 --- a/render.moon +++ b/render.moon @@ -1,13 +1,11 @@ -package.moonpath = './?.server.moon;' .. package.moonpath -import flush from require 'lib.init' -import render from require 'lib.mmmfs' +package.moonpath = './?.server.moon;./?/init.server.moon;' .. package.moonpath +import flush from require 'mmm.init' +import render from require 'mmm.mmmfs' -- usage: -- moon render.moon [ ] - { output_name, path, prefix_mod, prefix_path } = arg --- output_name = assert arg[1], "please specify the output filename as an argument" --- path = assert arg[2], "please specify the path name to build as an argument" + assert output_name, "please specify the output filename as an argument" assert path, "please specify the path name to build as an argument" @@ -32,10 +30,11 @@ with io.open output_name, 'w' MMM: lunar low-gravity scripting playground @@ -45,7 +44,7 @@ with io.open output_name, 'w' - + #{rehydrate} diff --git a/root/animations/init.moon b/root/animations/init.moon index ba8cb91..c968cfd 100644 --- a/root/animations/init.moon +++ b/root/animations/init.moon @@ -1,5 +1,5 @@ -import div, h3, ul, li, a from require 'lib.dom' -import define_fileders from require 'lib.mmmfs' +import div, h3, ul, li, a from require 'mmm.dom' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... diff --git a/root/animations/koch.moon b/root/animations/koch.moon index 9b8217d..4d16b8a 100644 --- a/root/animations/koch.moon +++ b/root/animations/koch.moon @@ -1,4 +1,4 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... with Fileder { @@ -9,8 +9,8 @@ with Fileder { if MODE == 'CLIENT' Math = window.Math - import CanvasApp from require 'lib.canvasapp' - import hsl from require 'lib.color' + import CanvasApp from require 'mmm.canvasapp' + import hsl from require 'mmm.color' class KochDemo extends CanvasApp width: 600 diff --git a/root/animations/twisted.moon b/root/animations/twisted.moon index 9e97738..68bdac6 100644 --- a/root/animations/twisted.moon +++ b/root/animations/twisted.moon @@ -1,4 +1,4 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... with Fileder { @@ -7,8 +7,8 @@ with Fileder { } if MODE == 'CLIENT' - import CanvasApp from require 'lib.canvasapp' - import hsl from require 'lib.color' + import CanvasApp from require 'mmm.canvasapp' + import hsl from require 'mmm.color' Math = window.Math diff --git a/root/articles/init.moon b/root/articles/init.moon index b706f7c..483aa32 100644 --- a/root/articles/init.moon +++ b/root/articles/init.moon @@ -1,5 +1,5 @@ -import div, h3, ul, li, a from require 'lib.dom' -import define_fileders from require 'lib.mmmfs' +import div, h3, ul, li, a from require 'mmm.dom' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... diff --git a/root/articles/mmmfs/gallery.moon b/root/articles/mmmfs/gallery.moon index e132f42..3abad43 100644 --- a/root/articles/mmmfs/gallery.moon +++ b/root/articles/mmmfs/gallery.moon @@ -1,5 +1,5 @@ -import define_fileders from require 'lib.mmmfs' -import div, h1, a, img, br from require 'lib.dom' +import define_fileders from require 'mmm.mmmfs' +import div, h1, a, img, br from require 'mmm.dom' Fileder = define_fileders ... @@ -23,7 +23,7 @@ with Fileder { div content 'slideshow: moon -> mmm/dom': => - import ReactiveVar, text, elements from require 'lib.component' + import ReactiveVar, text, elements from require 'mmm.component' index = ReactiveVar 1 diff --git a/root/articles/mmmfs/init.moon b/root/articles/mmmfs/init.moon index 2b3ef5e..8b1ce01 100644 --- a/root/articles/mmmfs/init.moon +++ b/root/articles/mmmfs/init.moon @@ -1,4 +1,4 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... @@ -11,7 +11,7 @@ Fileder { -- uses the 'moon ->' conversion to execute the lua/pre moon function on get -- resolves to a value of type mmm/dom 'moon -> mmm/dom': () => - html = require 'lib.dom' + html = require 'mmm.dom' import article, h1, h2, h3, p, div, a, sup, ol, li, span, code, pre, br from html moon = if MODE == 'SERVER' @@ -235,7 +235,7 @@ Fileder { 'title: text/plain': "Hey I'm like a link to picture or smth", 'URL -> image/png': 'https://picsum.photos/200?random', 'preview: moon -> mmm/dom': => - import img from require 'lib.dom' + import img from require 'mmm.dom' img src: @gett 'URL -> image/png' -- look for main content with 'URL to png' type } ]] @@ -264,7 +264,7 @@ If you are reading this in the source, then c'mon, just scroll past and give me -- preview is a lua/pre moon function that neturns an mmm/dom value 'preview: moon -> mmm/dom': => - import img from require 'lib.dom' + import img from require 'mmm.dom' img src: @gett 'URL -> image/png' -- look for main content with 'URL to png' type } diff --git a/root/articles/realities.moon b/root/articles/realities.moon index 05c0c03..78d7bae 100644 --- a/root/articles/realities.moon +++ b/root/articles/realities.moon @@ -1,11 +1,11 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... Fileder { 'name: alpha': 'realities' 'description: text/plain': 'exploring the nesting relationships of virtual and other realities' 'moon -> mmm/component': => - import elements from require 'lib.component' + import elements from require 'mmm.component' import h1, h2, p, a, i, div, ol, li, br, hr, span, button, section, article from elements _content = div! diff --git a/root/experiments/center_of_mass.moon b/root/experiments/center_of_mass.moon index 641fc5a..3d0e924 100644 --- a/root/experiments/center_of_mass.moon +++ b/root/experiments/center_of_mass.moon @@ -1,4 +1,4 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... with Fileder { @@ -7,9 +7,9 @@ with Fileder { } if MODE == 'CLIENT' - import CanvasApp from require 'lib.canvasapp' - import rgb from require 'lib.color' - import article, h1, p, div, span, input, button from require 'lib.dom' + import CanvasApp from require 'mmm.canvasapp' + import rgb from require 'mmm.color' + import article, h1, p, div, span, input, button from require 'mmm.dom' fast = true center = true diff --git a/root/experiments/init.moon b/root/experiments/init.moon index d9f4938..65f3852 100644 --- a/root/experiments/init.moon +++ b/root/experiments/init.moon @@ -1,5 +1,5 @@ -import div, h3, ul, li, a from require 'lib.dom' -import define_fileders from require 'lib.mmmfs' +import div, h3, ul, li, a from require 'mmm.dom' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... diff --git a/root/experiments/tags/init.moon b/root/experiments/tags/init.moon index f113e74..611d722 100644 --- a/root/experiments/tags/init.moon +++ b/root/experiments/tags/init.moon @@ -1,5 +1,5 @@ -import div, h3, ul, li, a from require 'lib.dom' -import define_fileders from require 'lib.mmmfs' +import div, h3, ul, li, a from require 'mmm.dom' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... @@ -8,7 +8,7 @@ Fileder { 'description: text/plain': 'defining toggles, categories etc. with only tags and functional hooks', 'moon -> mmm/component': if MODE == 'CLIENT' then => import add_tag, rmv_tag, Node, Hierarchy, Toggle, NamespacedToggle from require '.tags' - import ReactiveVar, tohtml, text, elements from require 'lib.component' + import ReactiveVar, tohtml, text, elements from require 'mmm.component' import article, div, form, span, h3, a, input, textarea, button from elements clone = (set) -> diff --git a/root/init.moon b/root/init.moon index a2eab3b..8639f50 100644 --- a/root/init.moon +++ b/root/init.moon @@ -1,12 +1,12 @@ -import define_fileders from require 'lib.mmmfs' +import define_fileders from require 'mmm.mmmfs' Fileder = define_fileders ... require = relative ... Fileder { 'name: alpha': '', 'moon -> mmm/dom': => - import article, h1, h3, div, b, p, a, br, ul, tt, li, img from require 'lib.dom' - import opairs from require 'lib.ordered' + import article, h1, h3, div, b, p, a, br, ul, tt, li, img from require 'mmm.dom' + import opairs from require 'mmm.ordered' append, finish = do content = {} @@ -65,4 +65,5 @@ Fileder { require '.articles' require '.animations' require '.experiments' + require '.meta' } diff --git a/root/meta/init.moon b/root/meta/init.moon new file mode 100644 index 0000000..cd2baed --- /dev/null +++ b/root/meta/init.moon @@ -0,0 +1,27 @@ +import div, h3, ul, li, a from require 'mmm.dom' +import define_fileders from require 'mmm.mmmfs' +Fileder = define_fileders ... +require = relative ... + +Fileder { + 'name: alpha': 'meta', + 'title: text/plain': 'about mmm', + 'moon -> mmm/dom': (path) => div { + h3 @gett 'title: text/plain', style: { 'margin-bottom': '-.5em' }, + ul for child in *@children + name = child\gett 'name: alpha' + desc = child\gett 'description: text/plain' + li { + a name, { + href: child.path, + onclick: (e) => + e\preventDefault! + BROWSER\navigate child.path + }, + ': ', desc + } + } + + require '.todo' + require '.test_component' +} diff --git a/root/meta/test_component.moon b/root/meta/test_component.moon new file mode 100644 index 0000000..c1c766e --- /dev/null +++ b/root/meta/test_component.moon @@ -0,0 +1,224 @@ +import define_fileders from require 'mmm.mmmfs' +Fileder = define_fileders ... + +Fileder { + 'name: alpha': 'mmm.component_test' + 'description: text/plain': 'Tests for mmm.component' + 'moon -> mmm/dom': => + import article, div, h1, ul, li, pre from require 'mmm.dom' + + _content = {} + append = (stuff) -> table.insert _content, stuff + + last = nil + test_group = (name) -> + if last + append div (h1 name), ul last + + last = {} + (name, test) -> + ok, err = pcall test + table.insert last, li if ok + "passed '#{name}'" + else + "failed '#{name}'", pre err + + expect = (expected, note, ...) -> + ok, msg = pcall ... + print ok, msg\find expected + if ok or not msg\find expected + error note + + run_test = test_group 'component.moon' + + local ReactiveVar, ReactiveElement + run_test "exports ReactiveVar, ReactiveElement", -> + import ReactiveVar, ReactiveElement from require 'mmm.component' + assert ReactiveVar, "ReactiveVar not exported" + assert ReactiveElement, "ReactiveElement not exported" + + run_test "exports tohtml helper", -> + import tohtml from require 'mmm.component' + assert 'function' == (type tohtml), "tohtml not exported" + + -- @TODO: get_or_create + + run_test "exports text helper", -> + import text from require 'mmm.component' + assert 'function' == (type text), "text not exported" + + node = text 'a test string' + assert (js.instanceof node, js.global.Node), "expected text to generate a Node" + assert node.data == 'a test string', "expected text to store the string" + + run_test "text joins multiple arguments", -> + import text from require 'mmm.component' + + node = text 'a', 'test', 'string' + assert node.data == 'a test string', "expected text to join arguments with spaces" + + run_test "exports elements table", -> + import elements from require 'mmm.component' + + assert (type elements.div!) == 'table', "expected to build element with elements.div!" + assert (type elements.madeup!) == 'table', "expected to build element with elements.madeup!" + + run_test = test_group 'ReactiveVar' + + run_test "stores a value", -> + reactive = ReactiveVar 'test' + assert 'test' == reactive\get!, "expected x to be 'test'" + + run_test "propagates updates", -> + local done + + reactive = ReactiveVar 'test' + reactive\subscribe coroutine.wrap (next) -> + assert next == 'toast', "expected next to be 'toast'" + assert coroutine.yield! == 'cheese', "expected next to be 'cheese'" + done = true + + reactive\set 'toast' + assert 'toast' == reactive\get!, "expected #get to return 'toast'" + reactive\set 'cheese' + assert done, "expected to reach the end" + + run_test "passed old value as well", -> + local done + + reactive = ReactiveVar 1 + reactive\subscribe coroutine.wrap (next, last) -> + assert last == 1, "expected last:1 to be 1" + next, last = coroutine.yield! + assert last == 2, "expected last:2 to be 2" + done = true + + reactive\set 2 + reactive\set 3 + assert done, "expected to reach the end" + + run_test "provides transform shorthand", -> + local done + + reactive = ReactiveVar 1 + reactive\subscribe coroutine.wrap (next, last) -> + assert last == 1, "expected last:1 to be 1" + next, last = coroutine.yield! + assert last == 2, "expected last:2 to be 2" + done = true + + add_one = (a) -> a + 1 + reactive\transform add_one + reactive\transform add_one + assert done, "expected to reach the end" + + run_test "#subscribe returns function to unsubscribe", -> + calls = 0 + + reactive = ReactiveVar 1 + unsub = reactive\subscribe coroutine.wrap () -> + calls += 1 + coroutine.yield! + calls += 1 + coroutine.yield! + calls += 1 + + assert 'function' == (type unsub), "expected to receive a function" + + reactive\set 2 + reactive\set 3 + assert calls == 2, "wat" + + unsub! + reactive\set 4 + assert calls == 2, "expected to stop receiving updates" + + run_test "tracks multiple subscriptions at once", -> + reactive = ReactiveVar 'test' + unsub = reactive\subscribe coroutine.wrap (next) -> + assert next == 'toast', "expected next to be toast" + next = coroutine.yield! + assert next == 'cheese', "expected next to be cheese" + coroutine.yield! + error "expected not to get here" + + reactive\set 'toast' + + local done + reactive\subscribe coroutine.wrap (next) -> + assert next == 'cheese', "expected next to be cheese" + next = coroutine.yield! + assert next == 'test', "expected next to be test" + done = true + + reactive\set 'cheese' + unsub! + reactive\set 'test' + assert done, "expected to reach the end" + + run_test = test_group 'ReactiveElement' + + run_test "creates a HTML element", -> + elem = ReactiveElement 'span' + assert elem.node and elem.node.localName == 'span', "expected Node to be a " + + -- @TODO: can take over a DOM Node + + run_test "sets attributes from a table arg", -> + elem = ReactiveElement 'span', class: 'never' + assert elem.node.class == 'never', "expected class to be 'never'" + + run_test "appends Nodes from arguments", -> + e_div, e_pre = div!, pre! + elem = ReactiveElement 'span', e_div, e_pre + assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" + assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem" + + run_test "can append ReactiveElements and text", -> + e_div = ReactiveElement 'div' + elem = ReactiveElement 'div', e_div, 'testtext' + assert elem.node.firstElementChild == e_div.node, "expected div to be the first child of elem" + assert elem.node.lastChild.data == 'testtext', "expected last child of elem to be 'testtext'" + + run_test "accepts attributes after children", -> + e_div = div! + elem = ReactiveElement 'div', e_div, class: 'test' + assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" + assert elem.node.class == 'test', "expected class to be 'test'" + + run_test "allows mixing attributes and children in a single table", -> + e_div, e_pre = div!, pre! + elem = ReactiveElement 'div', { class: 'test', e_div, e_pre } + assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem" + assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem" + assert elem.node.class == 'test', "expected class to be 'test'" + + run_test "can unwrap and track attributes from ReactiveVars", -> + klass = ReactiveVar 'test' + elem = ReactiveElement 'div', class: klass + assert elem.node.class == 'test', "expected class to be 'test'" + klass\set 'toast' + assert elem.node.class == 'toast', "expected class to be 'toast'" + + run_test "can unwrap and track children from ReactiveVars", -> + child = ReactiveVar h1 'test' + elem = ReactiveElement 'div', child, pre 'fixed' + assert elem.node.firstElementChild.localName == 'h1', "expected first child to be h1" + assert elem.node.childElementCount == 2, "expected node to have two children" + child\set div 'toast' + assert elem.node.firstElementChild.localName == 'div', "expected first child to be div" + assert elem.node.childElementCount == 2, "expected node to have two children" + + run_test "warns when appending a string from a ReactiveVar", -> + import text from require 'mmm.component' + + str = ReactiveVar 'test' + elem = ReactiveElement 'div', str + expect 'cannot replace string node', 'expected error', str\set, 'string too' + elem\destroy! + + elem = ReactiveElement 'div', str\map text + str\set 'this is text' + + article _content +} diff --git a/root/meta/todo.moon b/root/meta/todo.moon new file mode 100644 index 0000000..eb6fc0d --- /dev/null +++ b/root/meta/todo.moon @@ -0,0 +1,42 @@ +import define_fileders from require 'mmm.mmmfs' +Fileder = define_fileders ... + +Fileder { + 'name: alpha': 'todo' + 'description: text/plain': 'TodoMVC using mmm.component' + 'moon -> mmm/component': => + import ReactiveVar, text, elements from require 'mmm.component' + import article, div, form, span, h3, a, input, textarea, button from elements + + parent = div! + todoItem = (desc, done) -> + -- convert into reactive data sources + desc, done = (ReactiveVar desc), ReactiveVar done + with me = div style: + margin: '8px' + padding: '8px' + background: '#eeeeee' + \append h3 (desc\map text), style: 'margin: 0;' + \append span done\map (done) -> text if done then 'done' else 'not done yet' + \append input type: 'checkbox', checked: done, onchange: (e) => done\set e.target.checked + \append a (text 'delete'), href: '#', onclick: (e) => parent\remove me + + parent\append todoItem 'write a Component System', true + parent\append todoItem 'eat Lasagna', true + parent\append todoItem 'do other things' + + desc = ReactiveVar 'start' + form = with form { + action: '' + style: + margin: '2px' + onsubmit: (e) => + e\preventDefault! + parent\append todoItem desc\get! + desc\set '' + } + \append input type: 'text', value: desc, onchange: (e) => desc\set e.target.value + \append input type: 'submit', value: 'add' + + article parent, form +} diff --git a/tup.moon b/tup.moon index 033e0f6..cdee2d4 100644 --- a/tup.moon +++ b/tup.moon @@ -1,6 +1,6 @@ -package.moonpath = './?.server.moon;' .. package.moonpath +package.moonpath = './?.server.moon;./?/init.server.moon;' .. package.moonpath require 'lfs' -require 'lib.init' +require 'mmm.init' -- enumerate moonscript source files enum_dir = do @@ -19,18 +19,21 @@ enum_dir = do coroutine.wrap -> yieldtree dir +-- COMPILE MOONSCRIPT +-- mmmfs tree for file in enum_dir 'root' basename = assert file\match '(.*)%.moon$' print ": #{file} |> ^ MOON %b > %o^ moonc -o %o %f |> dist/#{basename}.lua" -for file in enum_dir 'lib' +-- mmm +for file in enum_dir 'mmm' continue if file\match '%.server%.moon$' basename = assert file\match '(.*)%.moon$' basename = (file\match '(.*)%.client') or basename print ": #{file} |> ^ MOON %b > %o^ moonc -o %o %f |> dist/#{basename}.lua" --- add rules for each fileder -import module_roots from require 'lib.mmmfs' +-- PRE-RENDER MMMFS +import module_roots from require 'mmm.mmmfs' root = require 'root' root\mount! -- cgit v1.2.3