diff --git a/build/server.moon b/build/server.moon index 33a985f..33853c9 100644 --- a/build/server.moon +++ b/build/server.moon @@ -8,6 +8,7 @@ add '?/init.server' require 'mmm' +require 'mmm.mmmfs' import dir_base, Key, Fileder from require 'mmm.mmmfs.fileder' import convert from require 'mmm.mmmfs.conversion' @@ -65,7 +66,7 @@ - + ]] render browser\todom!, fileder, noview: true, scripts: deps .. " diff --git a/mmm/mmmfs/browser.moon b/mmm/mmmfs/browser.moon index f232f2a..35925d3 100644 --- a/mmm/mmmfs/browser.moon +++ b/mmm/mmmfs/browser.moon @@ -1,18 +1,16 @@ require = relative ..., 1 import Key from require '.fileder' +import converts from require '.plugins' import get_conversions, apply_conversions from require '.conversion' import ReactiveVar, get_or_create, text, elements, tohtml from require 'mmm.component' import pre, div, nav, span, button, a, code, select, option from elements import languages from require 'mmm.highlighting' -converts = require '.converts' keep = (var) -> last = var\get! var\map (val) -> last = val or last last - -code_cast = (lang) -> casts = { { diff --git a/mmm/mmmfs/conversion.moon b/mmm/mmmfs/conversion.moon index 4700eea..c0505a8 100644 --- a/mmm/mmmfs/conversion.moon +++ b/mmm/mmmfs/conversion.moon @@ -1,5 +1,4 @@ require = relative ..., 1 -base_converts = require '.converts' import Queue from require '.queue' count = (base, pattern='->') -> select 2, base\gsub pattern, '' @@ -13,8 +12,9 @@ -- * want - stop type pattern -- * limit - limit conversion amount -- returns a list of conversion steps -get_conversions = (want, have, converts=base_converts, limit=5) -> +get_conversions = (want, have, converts=PLUGINS and PLUGINS.converts, limit=5) -> assert have, 'need starting type(s)' + assert converts, 'need to pass list of converts' if 'string' == type have have = { have } diff --git a/mmm/mmmfs/converts.moon b/mmm/mmmfs/converts.moon deleted file mode 100644 index f098448..0000000 --- a/mmm/mmmfs/converts.moon +++ /dev/null @@ -1,386 +0,0 @@ -require = relative ..., 1 -import div, pre, code, img, video, blockquote, a, span, source, iframe from require 'mmm.dom' -import find_fileder, link_to, embed from (require 'mmm.mmmfs.util') require 'mmm.dom' -import render from require '.layout' -import tohtml from require 'mmm.component' -import languages from require 'mmm.highlighting' - -keep = (var) -> - last = var\get! - var\map (val) -> - last = val or last - last - --- fix JS null values -js_fix = if MODE == 'CLIENT' - (arg) -> - return if arg == js.null - arg - --- limit function to one argument -single = (func) -> (val) => func val - --- load a chunk using a specific 'load'er -loadwith = (_load) -> (val, fileder, key) => - func = assert _load val, "#{fileder}##{key}" - func! - --- highlight code -code_hl = (lang) -> - { - inp: "text/#{lang}", - out: 'mmm/dom', - cost: 5 - transform: (val) => pre languages[lang] 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: 'fn -> (.+)', - out: '%1', - cost: 1 - transform: (val, fileder) => val fileder - } - { - inp: 'mmm/component', - out: 'mmm/dom', - cost: 3 - transform: single tohtml - } - { - inp: 'mmm/dom', - out: 'text/html+frag', - cost: 3 - transform: (node) => if MODE == 'SERVER' then node else node.outerHTML - } - { - -- inp: 'text/html%+frag', - -- @TODO: this doesn't feel right... maybe mmm/dom has to go? - inp: 'mmm/dom', - out: 'text/html', - cost: 3 - transform: (html, fileder) => render html, fileder - } - { - inp: 'text/html%+frag', - out: 'mmm/dom', - cost: 1 - transform: if MODE == 'SERVER' - (html, fileder) => - html = html\gsub '(.-)', (attrs, text) -> - text = nil if #text == 0 - path = '' - while attrs and attrs != '' - key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' - if not key - key, _attrs = attrs\match '^(%w+)%s*(.*)$' - val = true - - attrs = _attrs - - switch key - when 'path' then path = val - else warn "unkown attribute '#{key}=\"#{val}\"' in " - - link_to path, text, fileder - - html = html\gsub '(.-)', (attrs, desc) -> - path, facet = '', '' - opts = {} - if #desc != 0 - opts.desc = desc - - while attrs and attrs != '' - key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' - if not key - key, _attrs = attrs\match '^(%w+)%s*(.*)$' - val = true - - attrs = _attrs - - switch key - when 'path' then path = val - when 'facet' then facet = val - when 'nolink' then opts.nolink = true - when 'inline' then opts.inline = true - else warn "unkown attribute '#{key}=\"#{val}\"' in " - - embed path, facet, fileder, opts - - html - else - (html, fileder) => - parent = with document\createElement 'div' - .innerHTML = html - - -- copy to iterate safely, HTMLCollections update when nodes are GC'ed - embeds = \getElementsByTagName 'mmm-embed' - embeds = [embeds[i] for i=0, embeds.length - 1] - for element in *embeds - path = js_fix element\getAttribute 'path' - facet = js_fix element\getAttribute 'facet' - nolink = js_fix element\getAttribute 'nolink' - inline = js_fix element\getAttribute 'inline' - desc = js_fix element.innerText - desc = nil if desc == '' - - element\replaceWith embed path or '', facet or '', fileder, { :nolink, :inline, :desc } - - embeds = \getElementsByTagName 'mmm-link' - embeds = [embeds[i] for i=0, embeds.length - 1] - for element in *embeds - text = js_fix element.innerText - path = js_fix element\getAttribute 'path' - - element\replaceWith link_to path or '', text, fileder - - assert 1 == parent.childElementCount, "text/html with more than one child!" - parent.firstElementChild - } - { - inp: 'text/lua -> (.+)', - out: '%1', - cost: 0.5 - transform: loadwith load or loadstring - } - { - inp: 'mmm/tpl -> (.+)', - out: '%1', - cost: 1 - transform: (source, fileder) => - source\gsub '{{(.-)}}', (expr) -> - path, facet = expr\match '^([%w%-_%./]*)%+(.*)' - assert path, "couldn't match TPL expression '#{expr}'" - - (find_fileder path, fileder)\gett facet - } - { - inp: 'time/iso8601-date', - out: 'time/unix', - cost: 0.5 - transform: (val) => - year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$' - assert year, "failed to parse ISO 8601 date: '#{val}'" - os.time :year, :month, :day - } - { - inp: 'URL -> twitter/tweet', - out: 'mmm/dom', - cost: 1 - transform: (href) => - id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'" - if MODE == 'CLIENT' - with parent = div! - window.twttr.widgets\createTweet id, parent - else - div blockquote { - class: 'twitter-tweet' - 'data-lang': 'en' - a '(linked tweet)', :href - } - } - { - inp: 'URL -> youtube/video', - out: 'mmm/dom', - cost: 1 - transform: (link) => - id = link\match 'youtu%.be/([^/]+)' - id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)' - id or= link\match 'youtube.com/[ev]/([^/]+)' - id or= link\match 'youtube.com/embed/([^/]+)' - - assert id, "couldn't parse youtube URL: '#{link}'" - - iframe { - width: 560 - height: 315 - frameborder: 0 - allowfullscreen: true - frameBorder: 0 - src: "//www.youtube.com/embed/#{id}" - } - } - { - inp: 'URL -> image/.+', - out: 'mmm/dom', - cost: 1 - transform: (src, fileder) => img :src - } - { - inp: 'URL -> video/.+', - out: 'mmm/dom', - cost: 1 - transform: (src) => - -- @TODO: add parsed MIME type - video (source :src), controls: true, loop: true - } - { - inp: 'text/plain', - out: 'mmm/dom', - cost: 2 - transform: (val) => span val - } - { - inp: 'alpha', - out: 'mmm/dom', - cost: 2 - transform: single code - } - -- this one needs a higher cost - -- { - -- inp: 'URL -> .+', - -- out: 'mmm/dom', - -- transform: single code - -- } - { - inp: '(.+)', - out: 'URL -> %1', - cost: 5 - transform: (_, fileder, key) => "#{fileder.path}/#{key.name}:#{@from}" - } - { - inp: 'table', - out: 'text/json', - cost: 2 - transform: do - tojson = (obj) -> - switch type obj - when 'string' - string.format '%q', obj - when 'table' - if obj[1] or not next obj - "[#{table.concat [tojson c for c in *obj], ','}]" - else - "{#{table.concat ["#{tojson k}: #{tojson v}" for k,v in pairs obj], ', '}}" - when 'number' - tostring obj - when 'boolean' - tostring obj - when 'nil' - 'null' - else - error "unknown type '#{type obj}'" - - (val) => tojson val - } - { - inp: 'table', - out: 'mmm/dom', - cost: 5 - transform: do - 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 - - (tbl) => pre code deep_tostring tbl - } - code_hl 'javascript' - code_hl 'moonscript' - code_hl 'lua' - code_hl 'markdown' - code_hl 'css' -} - -if MODE == 'SERVER' - ok, moon = pcall require, 'moonscript.base' - if ok - _load = moon.load or moon.loadstring - table.insert converts, { - inp: 'text/moonscript -> (.+)', - out: '%1', - cost: 1 - transform: loadwith moon.load or moon.loadstring - } - - table.insert converts, { - inp: 'text/moonscript -> (.+)', - out: 'text/lua -> %1', - cost: 2 - transform: single moon.to_lua - } -else - table.insert converts, { - inp: 'text/javascript -> (.+)', - out: '%1', - cost: 1 - transform: (source) => - f = js.new window.Function, source - f! - } - -do - local markdown - if MODE == 'SERVER' - success, discount = pcall require, 'discount' - if not success - warn "NO MARKDOWN SUPPORT!", discount - - markdown = success and (md) -> - res = assert discount.compile md, 'githubtags' - res.body - else - markdown = window and window.marked and window\marked - - if markdown - table.insert converts, { - inp: 'text/markdown', - out: 'text/html+frag', - cost: 1 - transform: (md) => "
#{markdown md}
" - } - - table.insert converts, { - inp: 'text/markdown%+span', - out: 'mmm/dom', - cost: 1 - transform: if MODE == 'SERVER' - (source) => - html = markdown source - html = html\gsub '^$', '/span>' - else - (source) => - html = markdown source - html = html\gsub '^%s*

%s*', '' - html = html\gsub '%s*

%s*$', '' - with document\createElement 'span' - .innerHTML = html - } - -if MODE == 'CLIENT' and window.mermaid - window.mermaid\initialize { - startOnLoad: false - fontFamily: 'monospace' - } - - id_counter = 1 - table.insert converts, { - inp: 'text/mermaid-graph' - out: 'mmm/dom' - cost: 1 - transform: (source, fileder, key) => - id_counter += 1 - id = "mermaid-#{id_counter}" - with container = document\createElement 'div' - cb = (svg) => - .innerHTML = svg - .firstElementChild.style.width = '100%' - .firstElementChild.style.height = 'auto' - - window\setImmediate (_) -> - window.mermaid\render id, source, cb, container - } - -converts diff --git a/mmm/mmmfs/init.moon b/mmm/mmmfs/init.moon index 8aba86a..fc89d7f 100644 --- a/mmm/mmmfs/init.moon +++ b/mmm/mmmfs/init.moon @@ -1,9 +1,4 @@ -require = relative ... -import Key, Fileder from require '.fileder' -import Browser from require '.browser' +require = relative ..., 0 -{ - :Key - :Fileder - :Browser -} +export ^ +PLUGINS = require '.plugins' diff --git a/mmm/mmmfs/plugins/code.moon b/mmm/mmmfs/plugins/code.moon new file mode 100644 index 0000000..ec22f71 --- /dev/null +++ b/mmm/mmmfs/plugins/code.moon @@ -0,0 +1,16 @@ +import pre from require 'mmm.dom' +import languages from require 'mmm.highlighting' + +-- syntax-highlighted code +{ + converts: { + { + inp: 'text/([^ ]*).*' + out: 'mmm/dom' + cost: 5 + transform: (val) => + lang = @from\match @convert.inp + pre languages[lang] val + } + } +} diff --git a/mmm/mmmfs/plugins/init.moon b/mmm/mmmfs/plugins/init.moon new file mode 100644 index 0000000..af4a2aa --- /dev/null +++ b/mmm/mmmfs/plugins/init.moon @@ -0,0 +1,292 @@ +require = relative ..., 1 +import div, pre, code, img, video, span, source from require 'mmm.dom' +import find_fileder, link_to, embed from (require 'mmm.mmmfs.util') require 'mmm.dom' +import render from require '.layout' +import tohtml from require 'mmm.component' + +keep = (var) -> + last = var\get! + var\map (val) -> + last = val or last + last + +-- fix JS null values +js_fix = if MODE == 'CLIENT' + (arg) -> + return if arg == js.null + arg + +-- limit function to one argument +single = (func) -> (val) => func val + +-- load a chunk using a specific 'load'er +loadwith = (_load) -> (val, fileder, key) => + func = assert _load val, "#{fileder}##{key}" + func! + +-- 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. +-- * cost - conversion cost +-- * transform - function (val: inp, fileder) => val: out +-- @convert, @from, @to contain the convert and the concrete types +converts = { + { + inp: 'fn -> (.+)', + out: '%1', + cost: 1 + transform: (val, fileder) => val fileder + } + { + inp: 'mmm/component', + out: 'mmm/dom', + cost: 3 + transform: single tohtml + } + { + inp: 'mmm/dom', + out: 'text/html+frag', + cost: 3 + transform: (node) => if MODE == 'SERVER' then node else node.outerHTML + } + { + -- inp: 'text/html%+frag', + -- @TODO: this doesn't feel right... maybe mmm/dom has to go? + inp: 'mmm/dom', + out: 'text/html', + cost: 3 + transform: (html, fileder) => render html, fileder + } + { + inp: 'text/html%+frag', + out: 'mmm/dom', + cost: 1 + transform: if MODE == 'SERVER' + (html, fileder) => + html = html\gsub '(.-)', (attrs, text) -> + text = nil if #text == 0 + path = '' + while attrs and attrs != '' + key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' + if not key + key, _attrs = attrs\match '^(%w+)%s*(.*)$' + val = true + + attrs = _attrs + + switch key + when 'path' then path = val + else warn "unkown attribute '#{key}=\"#{val}\"' in " + + link_to path, text, fileder + + html = html\gsub '(.-)', (attrs, desc) -> + path, facet = '', '' + opts = {} + if #desc != 0 + opts.desc = desc + + while attrs and attrs != '' + key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' + if not key + key, _attrs = attrs\match '^(%w+)%s*(.*)$' + val = true + + attrs = _attrs + + switch key + when 'path' then path = val + when 'facet' then facet = val + when 'nolink' then opts.nolink = true + when 'inline' then opts.inline = true + else warn "unkown attribute '#{key}=\"#{val}\"' in " + + embed path, facet, fileder, opts + + html + else + (html, fileder) => + parent = with document\createElement 'div' + .innerHTML = html + + -- copy to iterate safely, HTMLCollections update when nodes are GC'ed + embeds = \getElementsByTagName 'mmm-embed' + embeds = [embeds[i] for i=0, embeds.length - 1] + for element in *embeds + path = js_fix element\getAttribute 'path' + facet = js_fix element\getAttribute 'facet' + nolink = js_fix element\getAttribute 'nolink' + inline = js_fix element\getAttribute 'inline' + desc = js_fix element.innerText + desc = nil if desc == '' + + element\replaceWith embed path or '', facet or '', fileder, { :nolink, :inline, :desc } + + embeds = \getElementsByTagName 'mmm-link' + embeds = [embeds[i] for i=0, embeds.length - 1] + for element in *embeds + text = js_fix element.innerText + path = js_fix element\getAttribute 'path' + + element\replaceWith link_to path or '', text, fileder + + assert 1 == parent.childElementCount, "text/html with more than one child!" + parent.firstElementChild + } + { + inp: 'text/lua -> (.+)', + out: '%1', + cost: 0.5 + transform: loadwith load or loadstring + } + { + inp: 'mmm/tpl -> (.+)', + out: '%1', + cost: 1 + transform: (source, fileder) => + source\gsub '{{(.-)}}', (expr) -> + path, facet = expr\match '^([%w%-_%./]*)%+(.*)' + assert path, "couldn't match TPL expression '#{expr}'" + + (find_fileder path, fileder)\gett facet + } + { + inp: 'time/iso8601-date', + out: 'time/unix', + cost: 0.5 + transform: (val) => + year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$' + assert year, "failed to parse ISO 8601 date: '#{val}'" + os.time :year, :month, :day + } + { + inp: 'URL -> image/.+', + out: 'mmm/dom', + cost: 1 + transform: (src, fileder) => img :src + } + { + inp: 'URL -> video/.+', + out: 'mmm/dom', + cost: 1 + transform: (src) => + -- @TODO: add parsed MIME type + video (source :src), controls: true, loop: true + } + { + inp: 'text/plain', + out: 'mmm/dom', + cost: 2 + transform: (val) => span val + } + { + inp: 'alpha', + out: 'mmm/dom', + cost: 2 + transform: single code + } + -- this one needs a higher cost + -- { + -- inp: 'URL -> .+', + -- out: 'mmm/dom', + -- transform: single code + -- } + { + inp: '(.+)', + out: 'URL -> %1', + cost: 5 + transform: (_, fileder, key) => "#{fileder.path}/#{key.name}:#{@from}" + } + { + inp: 'table', + out: 'text/json', + cost: 2 + transform: do + tojson = (obj) -> + switch type obj + when 'string' + string.format '%q', obj + when 'table' + if obj[1] or not next obj + "[#{table.concat [tojson c for c in *obj], ','}]" + else + "{#{table.concat ["#{tojson k}: #{tojson v}" for k,v in pairs obj], ', '}}" + when 'number' + tostring obj + when 'boolean' + tostring obj + when 'nil' + 'null' + else + error "unknown type '#{type obj}'" + + (val) => tojson val + } + { + inp: 'table', + out: 'mmm/dom', + cost: 5 + transform: do + 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 + + (tbl) => pre code deep_tostring tbl + } +} + +add_converts = (module) -> + ok, plugin = pcall require, ".plugins.#{module}" + + if not ok + print "[Plugins] couldn't load plugins.#{module}: #{plugin}" + return + + print "[Plugins] loaded plugins.#{module}" + + if plugin.converts + for convert in *plugin.converts + table.insert converts, convert + +add_converts 'code' +add_converts 'markdown' +add_converts 'mermaid' +add_converts 'twitter' +add_converts 'youtube' + +if MODE == 'SERVER' + ok, moon = pcall require, 'moonscript.base' + if ok + _load = moon.load or moon.loadstring + table.insert converts, { + inp: 'text/moonscript -> (.+)', + out: '%1', + cost: 1 + transform: loadwith moon.load or moon.loadstring + } + + table.insert converts, { + inp: 'text/moonscript -> (.+)', + out: 'text/lua -> %1', + cost: 2 + transform: single moon.to_lua + } +else + table.insert converts, { + inp: 'text/javascript -> (.+)', + out: '%1', + cost: 1 + transform: (source) => + f = js.new window.Function, source + f! + } + +:converts diff --git a/mmm/mmmfs/plugins/markdown.moon b/mmm/mmmfs/plugins/markdown.moon new file mode 100644 index 0000000..5024320 --- /dev/null +++ b/mmm/mmmfs/plugins/markdown.moon @@ -0,0 +1,33 @@ +markdown = if MODE == 'SERVER' + success, discount = pcall require, 'discount' + assert success, "couldn't require 'discount'" + + (md) -> + res = assert discount.compile md, 'githubtags' + res.body +else + assert window and window.marked, "marked.js not found" + window\marked + +assert markdown, "no markdown implementation found" + +{ + converts: { + { + inp: 'text/markdown' + out: 'text/html+frag' + cost: 1 + transform: (md) => "
#{markdown md}
" + } + { + inp: 'text/markdown%+span' + out: 'text/html+frag' + cost: 1 + transform: (source) => + html = markdown source + html = html\gsub '^%s*

%s*', '' + html = html\gsub '%s*

%s*$', '' + html + } + } +} diff --git a/mmm/mmmfs/plugins/mermaid.moon b/mmm/mmmfs/plugins/mermaid.moon new file mode 100644 index 0000000..ae34afa --- /dev/null +++ b/mmm/mmmfs/plugins/mermaid.moon @@ -0,0 +1,29 @@ +assert window and window.mermaid, "mermaid.js not found" + +window.mermaid\initialize { + startOnLoad: false + fontFamily: 'monospace' +} + +id_counter = 1 + +{ + converts: { + { + inp: 'text/mermaid-graph' + out: 'mmm/dom' + cost: 1 + transform: (source, fileder, key) => + id_counter += 1 + id = "mermaid-#{id_counter}" + with container = document\createElement 'div' + cb = (svg) => + .innerHTML = svg + .firstElementChild.style.width = '100%' + .firstElementChild.style.height = 'auto' + + window\setImmediate (_) -> + window.mermaid\render id, source, cb, container + } + } +} diff --git a/mmm/mmmfs/plugins/twitter.moon b/mmm/mmmfs/plugins/twitter.moon new file mode 100644 index 0000000..b6e1adc --- /dev/null +++ b/mmm/mmmfs/plugins/twitter.moon @@ -0,0 +1,22 @@ +import div, blockquote, a from require 'mmm.dom' + +{ + converts: { + { + inp: 'URL -> twitter/tweet' + out: 'mmm/dom' + cost: 1 + transform: (href) => + id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'" + if MODE == 'CLIENT' + with parent = div! + window.twttr.widgets\createTweet id, parent + else + div blockquote { + class: 'twitter-tweet' + 'data-lang': 'en' + a '(linked tweet)', :href + } + } + } +} diff --git a/mmm/mmmfs/plugins/youtube.moon b/mmm/mmmfs/plugins/youtube.moon new file mode 100644 index 0000000..99cf995 --- /dev/null +++ b/mmm/mmmfs/plugins/youtube.moon @@ -0,0 +1,27 @@ +import iframe from require 'mmm.dom' + +{ + converts: { + { + inp: 'URL -> youtube/video' + out: 'mmm/dom' + cost: 1 + transform: (link) => + id = link\match 'youtu%.be/([^/]+)' + id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)' + id or= link\match 'youtube.com/[ev]/([^/]+)' + id or= link\match 'youtube.com/embed/([^/]+)' + + assert id, "couldn't parse youtube URL: '#{link}'" + + iframe { + width: 560 + height: 315 + frameborder: 0 + allowfullscreen: true + frameBorder: 0 + src: "//www.youtube.com/embed/#{id}" + } + } + } +}